Shader教程

这节贯穿创建你自己的shader的整个过程。这不是世界上最有用的shader,但它展示了如何得到shader,哪些工具能被使用,以及为你将来的可能更令人兴奋的shader做准备。

看一看图6-4你要前进的目标,它展示了来自于上一章的apple模型,不过这一次你的新shader文件SimpleShader.fx 应用于其上,另一个纹理被用来作为它上面的一些文本。

1
图6-4

SimpleShader.fx使用了一个normal vertex shader,它只转换3D数据,一个对于pixel shader而言简单的镜面反射 per-pixel 技法,pixel shader为之处理每一个像素、计算周围的环境、漫射、镜面反射成分。因为你不能对pixel shader 规划任何特殊规则,并且展示镜面反射光特效唯一一种方式就是用vertex shader去做,所以这种计算使用固定功能shader是不可能的。你可能要问为什么在vertex shader中计算镜面反射成分很糟糕。好的,如果你有一个低多边形的对象,就像这里的apple,或者来自于较早的normal mapping 范例的球体,仅仅在vertex shader中计算色彩成分将看上去很糟糕,因为你只能为每个顶点计算合成出来的颜色。如果你看一看apple的线框(如图6-5)你能看到那只有一串顶点(这些点连接成线),之间的所有数据不会以正确的方式计算,只能以内插值替换。但是如果像在图6-4中的这样一束高光在两个顶点之间,就根本不会被看到。顺便说一下,如果你想以线框模式渲染,只要在你渲染任何3D数据之前,添加下列代码行:

BaseGame.Device.RenderState.FillMode = FillMode.WireFrame;

2
图6-5

FX Composer

为了从仅有的一点SimpleShader.fx shader起步,你将是使用可以免费得到的 FX Composer 工具,你能在Nvidia 主页(www.Nvidia.com)的developers部分下载它(或者Google找到它)。

在你安装和启动FX Composer之后,你将看到如图6-6所示的屏幕。 它展示给你几个panel面板,甚至能被艺术家用来测试texture纹理、shader技法、修改shader 参数诸如color值、特效强度以及更多。对于你作为开发者最重要的panel面板是在中间的, 它显示了当前被选择的.fx文件(类似于Visual Studio)的源代码。 如你所见,.fx 文件看上去非常类似于 C# 或 CPP,它有语法高亮区和看上去类似于C#的许多关键字。例如,string 就是一个文本字符串;float 就是一个浮点数等等就像在C#中。不过也有其他类型例如float3或者float4x4。这些在float 名后面的数字仅仅表明次元;float3和XNA中的Vector3同样结构,它也包含3个浮点数(x、y、z)。Float4x4 描述了一个矩阵并且容纳16 个float数值(4×4);它也和 XNA的Matrix结构是同一种格式。你必须知道的最后一些变量类型是texture纹理。Textures被定义为纹理类型,你还必须具体指定一个样本来告诉shader如何使用这个shader(使用时筛选texture尺寸等等)。

3
图6-6

通过一个专栏,在上面你看到可选的semantic值(WorldViewProjection、Diffuse、Direction等等)后面显示的变量。它们告诉FX Composer 如何填充以及如何使用该值。 对于你的 XNA 程序,这些值没什么,指定这些semantic总是非常普通的。它允许你在其他程序,诸如FX Composer、3D Studio Max中使用shader,当你在后面通读了shader的时候它也很有用。semantic告诉你规定要用的确切的数值。在XNA中你不受应用的限制;例如,你能把world matrix 设置为viewInverse逆视野,不过一会儿就将令人困惑,不是么?

其他panel面板现在对你不那么重要,不过这里有每一面板的说明:

  • 使用顶部的工具条来快速加载一个shader,以及保存你当前的文件。工具条的最后一个按钮是build构建你的shader,并且在底部的log面板显示你的任何编译错误(非常像Visual Studio)。你每一次构建shader它都自动保存,你应该尽可能地时常使用(或者热键Ctrl+F7)该按钮以确保你的shader总是被编译和保存。

  • 左边的Materials 面板显示给你一个所有你当前能加载到FX Composer中的shader列表,一个shader带有一个小预览球,你一改变任何shader,球体就随之改变。这里最重要的按钮是“Assign to Selection”,把材质设置给当前被选择的Scene面板对象。

  • Textures 面板显示了当前shader使用的纹理。如果你加载一些外部的shader、texture文件经常找不到是因为它们存在于另一个文件夹或者不匹配.fx shader文件。那些不能被加载的Texture被显示为全蓝的bitmap位图,你首先应该确保加载纹理,否则shader输出通常是黑的和无效的。

  • 在右边的Properties 面板显示了shader中你能设置的所有参数(如果不是,它们已经被FX Composer填充了,像world、viewInverse等等)。要让shader在你的XNA引擎中以同样的方式工作,你必须也在游戏引擎中设置所有这些值,特别是在3D世界中依赖于当前摄像机位置、viewport、object的矩阵。如果你不设置参数,就使用.fx文件源代码中直接被设置的默认值。为了确保所有参数总是合法数值,举个例子,即使用户或者引擎没有设置任何颜色参数,你应该始终确保这些参数是有用的默认设置,如漫反射颜色设置为白色:

    float4 diffuseColor : Diffuse = {1.0f, 1.0f, 1.0f, 1.0f};
  • 最后,Scene 面板显示了一个像标准球体的简单范例对象来测试你的shader。你也能把它改变为一个立方体、圆柱体或者其他。二者择一地,你甚至能导入模型文件,并且四周播放它们,不过大多数文件不能很好工作,当你导入场景的时候,在FX Composer中的摄像机总是陷入困境。我正好坚持标准球体对象,在你的XNA引擎中进行所有高级的测试。FX Composer 2.0 在加载和处理自定义3D数据和模型文件上将会好很多。你可以使用Collada 文件,甚至管理所有的shader,这些shader被你模型中的每一个mesh所使用。如果你读到这里,可以获得FX Composer 2.0 就用它。
FX文件布局

如果你以前作过OpenGL,就不得不亲自编写顶点和 fragment shader(在DirectX中就意味着vertex和pixel shader),你将很高兴听到.fx文件把所有的shader代码放在一个地方,在你的.fx 文件中有许多不同的 pixel shader 语句块来支持多重目标结构(multiple target configurations)比如对应于GeForce 3 的pixel shader 1.1、为了支持Nvidia GeForce FX 或者 ATI Radeon 9x 就是pixel shader 2.0。具有在一个.fx文件中支持多重vertex和pixel shader的能力有另一个有用的次要影响,就是把相似的shader一起放置,并且它们都使用一些公有的方法和相同的shader参数,这使得shader开发更加容易。举个例子,如果你有一个normal mapping shader用于光亮的金属表面,它看起来就像金属,你可能需要另一个有更多的漫反射外观的shader用于石头,你可以把不同的shader技法(shader technique)放到同一个shader文件,然后在你的引擎中基于你想展示的材质选择习惯的技法。

作为一个典型的.fx 文件范例,图6-7展示了SimpleShader.fx 文件的规划层次。更多的复杂shader有更多的vertex shader和pixel shaders 以及更多的 technique技法。你不必对每一个技法创建新的vertex shader或者pixel shader;你能用你选购要的方式组合它们。一些shader可能也使用了multiple Pass,这意味着它们渲染1st Pass传入的一切,然后2nd pass 再次渲染完全相同的数据,来给材质添加更多的特效或层次。使用multiple Pass通常对于实时应用程序来说太慢,因为10 个pass的渲染意味着相同shader下,你的渲染时间将10倍高于仅仅渲染一个Pass。有时候使用multiple Pass可能很有用:使用multiple Pass来实现一些以别的方式不适应于shader指令限制的特效,或者对于你能使用由1st pass产生的,并且在2nd pass修改的post screen shader,来实现更好得多的effect特效。举个例子,模糊开销了大量的指令,因为要得到一个好的模糊结果,你必须混合许多像素来计算每一个模糊的点。

4
图6-7

例如,10×10 范围的模糊将开销100个像素读取指令,如果整个屏幕上你有一两百万个像素想要模糊听起来可不好。既然这样,仅仅在X方向模糊然后得到结果,second pass在Y方向上用另一系列的10个像素读取指令来模糊它,这样做要好很多。现在你的shader运行快了5倍,并且看起来几乎相同。从1600×1200的背景图片首先采样成400×300,然后执行模糊,你甚至有更好的执行效能产生,这将给你带来16倍的效能提高(现在令人吃惊吧)。

第八章谈论post screen shader;不过首先编写SimpleShader.fx文件吧。如你在图 6-7 所见,shader 文件使用相当多的shader参数。一些参数不是那么重要,因为你也能直接给shader进行硬编码材质设定,而且这种方式使你能在引擎中改变材质的颜色和外观,并且你能对许多不同的材质使用shader。其他参数,比如matrices和texture 非常重要,如果引擎不设置这些参数你就不能使用shader。在引擎中,材质数据,比如mcolor和texture值,应该在你创建shader的时候被加载,并且world matrix世界矩阵、light direction光照方向等等应该被每一帧设置,因为这些数据可能每一帧都变化。

参数

如果你想紧跟shader创建的步伐,你可能想现在打开FX Composer ,开启一个空白的.fx 文件。 选择File-〉New 来创建一个新的空白.fx 文件,并且删除所有内容。你将开启一个完全空白的.fx 文件。

你可能想做的第一件事就是快速回忆起关于这个文件的情况,稍后当你打开它时,在文件顶部添加一行描述或者注释:

// Chapter 6: Writing a simple shader for XNA

正如你在SimpleShader.fx 文件概览中所见的,你首先需要几个矩阵来转换vertex shader中的3D数据。这可能是worldViewProj矩阵、world matrix、和viewInverse matrix。worldViewProj matrix 结合了world matrix,后者把你想渲染的对象放到世界中正确的位置;view matrix,把3D 数据转换到你的摄像机(view space)视图空间(如上一章图 5-7 );最后,projection matrix,把(view space point)视图空间点放到正确的屏幕位置。该矩阵允许你仅仅使用一个矩阵乘法操作,就把input位置快速转换为最终的output位置。world matrix is then used to perform operations 3D世界中的 比如计算 world normal、lighting calculations等等。viewInverse通常只是用来得到更多关于摄像机位置的信息,这个信息能通过4th行得到的矩阵萃取:

float4x4 worldViewProj : WorldViewProjection;
float4x4 world : World;
float4x4 viewInverse : ViewInverse;

这些矩阵的每一个值都是一个float4×4类型(这是和XNA中的矩阵相同的数据类型),为了获得诸如FX Composer或者3D Studio Max这些应用程序的支持,你最好使用shader semantic来描述这些值,当模型塑造者想看一看带有shader的3D模型看上去如何的时候,这样做是非常重要的。很酷的一件事是,无论在FX Composer、3D Studio还是在你的引擎中,模型看上去绝对相同。这个事实能节约你大量的游戏开发时间;特别缩短了要正确得到3D对象的所有外观的测试过程。

现在是保存文件的时候了。只要按 Build 按钮,或者 Ctrl+F7 ,你被提醒输入一个新shader的名字。就命名为SimpleShader.fx ,把它放到你的XnaGraphicEngine内容文件夹,这样,你以后在XNA中可以快速使用它。在保存之后,FX Composer将在源代码的Tasks panel下告诉你“There were no techniques” 和“Compilation failed”。好吧,马上你将实现这些technique技法,不过首先要实现剩余的参数。因为你的shader使用了一个light使得你的apple加亮 (如图 6-4)你需要一个light,它可以是一个点光源或者一个方向光源。使用点光源有点儿更复杂,因为你必须为每一个单独的顶点计算光线方向(如果你喜欢,甚至可以对每一个单独的像素)。如果你使用一盏聚光灯,计算甚至变得更加复杂。点光源的另一个问题是它们通常随着距离衰减,如果你的3D世界很大,将需要大量灯光。Directional light方向光源简单得多,对于快速模拟室外环境的太阳非常有用,比如在下面几章中你将创建的游戏。

float3 lightDir : Direction
<
  string Object = "DirectionalLight";
  string Space = "World";
> = { 1, 0, 0 };

除了仅仅给所有材质添加的常规亮度的环境光,下列光源类型通常被使用在游戏中:

  • Directional Lights:方向性光,最简单的光源类型,非常易于实现。你能在shader中直接使用光源方向进行内部光源计算。在现实世界中没有方向性光存在;甚至太阳也是一个巨大的遥远的点光源,不过它很易于实现户外光照场景。

  • Point Lights: 点光源,计算一个单独的点光源不是非常困难,但是你不得不去计算超过一定距离使光源衰减的下降程度,万一你需要光源的方向,它也需要在shader中被计算,它会降低速度。不过点光源的主要问题是对于任何一个比房间大一点的场景,你需要超过1个点光源。3D射击游戏通常使用技巧来限制在同一时间内被看到的点光源的数量,但是对于户外游戏比如策略游戏,使用一个方向性的光源,仅添加稍稍几个的光照特效,以及对一些特殊效果使用简单的点光源,要简单得多。

  • Spot Lights: 聚光灯,与点光源是同样的事物,它们仅仅指向一个方向,并且由于光照圆锥计算,只照亮一个小的区域。聚光灯计算起来有一点更困难,但是如果你能忽略光照计算的困难部分(例如,当使用一个带有多重聚光灯的复杂的normal mapping shader 法线映射着色器),它甚至可以比使用点光源快很多。目前,你在Shader Model 3.0中仅能做条件语句比如“if”,前面几个shader版本也支持这些语句,但是所有的“if”语句和“for”循环只是正好被解开和展开,你不能像在Shader Model 3.0中获得很大的执行效能受益。

现在前面的代码看上去有点儿更复杂了;第一行几乎和矩阵一样。float3 明确说明你使用了一个Vector3 ,Direction 告诉你lightDir 被应用为一个方向性光源。其括号内部定义了一个Object和Space 型变量。这些变量被称为annotations (评注),它们规定了FX Composer 或者3D Studio Max这些其他程序参数的使用。这些程序现在知道如何使用该值,它们将自动把它分配给可能已经在场景中存在的light对象。你刚好能用这种方式在一个3D程序中加载shader文件,并且不必手工连接所有的light、材质设定、纹理就立刻工作。

接下来你将定义材质设定;你将使用的材质设定和标准的DirectX 材质所采用的相同。你能以这种方式在3D Studio Max等程序中使用类似的shader,或者以往的 DirectX材质,并且所有的color值被自动地正确应用。在引擎中你通常设置环境(ambient)和散射(diffuse)颜色,有时你也对于镜面颜色计算指定不同的光亮值(shininess value )。你也许注意到这里你不再使用任何 评注(annotation) - 你也可以在这里指定,不过即使你不定义annotation评注,材质设定在 FX Composer 和 3D Studio Max 中都工作良好。引擎也仅仅使用默认值 万一你以后不想为单元测试重写默认值。

float4 ambientColor : Ambient = { 0.2f, 0.2f, 0.2f, 1.0f };
float4 diffuseColor : Diffuse = { 0.5f, 0.5f, 0.5f, 1.0f };
float4 specularColor : Specular = { 1.0, 1.0, 1.0f, 1.0f };
float shininess : SpecularPower = 24.0f;

最后,你的shader需要一个纹理,看上去比显示一个灰色球体或苹果更有点儿趣味。取代使用apple纹理就像来自于上一章那样的原始苹果,你将使用一个新的测试纹理,在下一章当你添加normal mapping的时候,它将变得更有趣。纹理调用了marble.dds (如图 6-8):

texture diffuseTexture : Diffuse
<
  string ResourceName = "marble.dds";
>;
sampler DiffuseTextureSampler = sampler_state
{
  Texture = <diffuseTexture>;
  MinFilter=linear;
  MagFilter=linear;
  MipFilter=linear;
};

5
图6-8

ResourceName 评注(annotation)仅仅使用在 FX Composer,并且它从shader所在的同一个文件夹中自动加载 marble.dds 文件 (确定marble.dds 文件也在 XnaGraphicEngine 的content 文件夹)。这个例子只是具体指定你想对纹理使用linear filtering线性过滤。

Vertex Input Format

在你最后编写vertex shader 和 pixel shader 之前,你必须指定顶点数据在游戏和vertex shader之间被传入的方式,它被处理为VertexInput 结构体。它使用和XNA的VertexPositionNormalTexture结构体相同的数据,它被应用于apple模型。借助于前面定义的的worldViewProj matrix ,后面在vertex shader 中转换了apple的position 位置。texture coordinate 仅仅用来得到每一个你稍后将在pixel shader中渲染的像素的纹理坐标,并且为了光照计算需要normal(法线)值。

你应该始终确保你的游戏代码和shader使用了完全相同的顶点输入格式。如果你不这么做,错误数据可能应用在纹理坐标,或者顶点数据可能缺失,渲染起来一团糟。最好的实行是在应用程序中定义你自己的顶点结构(见下一章的TangentVertex ),然后在shader中定义同样的顶点结构。在你的游戏代码调用shader之前,还要设置使用的顶点声明,它描述了顶点结构体的规划。在下一章,你能找到更多关于此的细节。

struct VertexInput
{
  float3 pos : POSITION;
  float2 texCoord : TEXCOORD0;
  float3 normal : NORMAL;
};

你还是必须以相似的方式定义从vertex shader 传入到pixel shader的数据。起先也许听起来不熟悉,我向你许诺这是最后你必须去做的一件事,就是最终得到shader代码。如果你看一看图6-9,你能看到3D几何学经历的路线,从你的应用程序content数据到shader,shader使用图形硬件来以你想要的方式最终在屏幕上结束。虽然这整个过程比起在DirectX的旧岁月仅仅使用固定功能管道更加复杂,但是它允许你对每一个顶点优化代码,并且你能在每一步(无论在应用程序级别,还是动态的,当顶点在vertex shader被处理或者当它在屏幕被渲染的时候你能更改最终的像素颜色)更改数据。

6
图6-9

你的shader的VertexOutput 结构体传递了被转换的 vertex 位置、被应用的纹理的texture coordinate、一个normal法线和 halfVec 向量,为了直接在pixel shader中执行镜面反射颜色计算。这两个向量都必须被作为 texture coordinate传递 ,因为 从vertex 到pixel shader传递的数据只能是 position位置、color颜色、或者 texture coordinate数据。不过那没关系;你仍然能使用VertexInput 结构体中的相同形式的数据。它对于告诉 FX Composer、你的应用程序、或者任何其它使用shader的程序,在VertexInput 结构体中使用正确的semantics (Position、TexCoord0,和 Normal)非常重要。

因为你亲自定义了 VertexOutput 结构体,并且它只被shader内部使用,你能把任何你想要的放到这里,不过你应该尽可能让它保持短小,你也被你能忽略的pixel shader 的 texture coordinate的数目所限制(在pixel shader 1.1中是4,在 pixel shader 2.0中是8)。

struct VertexOutput
{
  float4 pos : POSITION;
  float2 texCoord : TEXCOORD0;
  float3 normal : TEXCOORD1;
  float3 halfVec : TEXCOORD2;
};
Vertex Shader

vertex shader 现在抓取了VertexInput数据,并且为了pixel shader把它转换为屏幕位置,pixel shader最终为每一个可见的多边形的点渲染输出的像素。vertex shader的最前面几行通常看起来和每一个其他的vertex shader极其相似,不过为了用在pixel shader中,你经常在vertex shader结尾预先估计数值。如果你正在使用pixel shader 1.1 你不能做某些事 比如 normalizing vectors或者执行复杂的数学函数如power(指数函数)。不过即使你使用 pixel shader 2.0 (就像你为这个shader) 你可能想预先估计某些数值来提高pixel shader的速度,pixel shade被每一个单独的可视的像素所执行。通常比起像素,你会拥有少得多的顶点,并且你在vertex shader 进行每一个复杂的计算能成倍加快pixel shader的执行效率。

// Vertex shader
VertexOutput VS_SpecularPerPixel(VertexInput In)
{
  VertexOutput Out = (VertexOutput)0;
  float4 pos = float4(In.pos, 1);
  Out.pos = mul(pos, worldViewProj);
  Out.texCoord = In.texCoord;
  Out.normal = mul(In.normal, world);
  // Eye pos
  float3 eyePos = viewInverse[3];
  // World pos
  float3 worldPos = mul(pos, world);
  // Eye vector
  float3 eyeVector = normalize(eyePos-worldPos);
  // Half vector
  Out.halfVec = normalize(eyeVector+lightDir);

  return Out;
} // VS_SpecularPerPixel(In)

vertex shader 拿 VertexInput 结构体当作为一个参数,它被自动填充,并且经由你稍后将在.fx文件末尾定义的shader technique 从3D应用程序数据中传递过来。这里重要的部分是VertexOutput 结构,它从 vertex shader 中被返回,然后传递给 pixel shader。数据不是正好被 1:1 传递为pixel shader的,而是在每一个单独的多边形点之间,所有数值是以内插值替换的(如图 6-10)。

7
图6-10

对于任何位置和颜色值,这个过程是一件好事,因为当数值被正确的插入,输出看起来会好得多。不过万一你使用normalized 向量,就会被GPU自动进行的插补处理搞乱。为了修正之,你必须在pixel shader(如图6-11)中re-normalize (重新法线化)向量。有时候他能被忽略,因为加工品不可见,要不是你的镜面反射每像素计算将被每一个低多边形数目的对象可见。如果你使用pixel shader 1.1你不能在pixel shader 中使用normalize method 方法。你可以使用一个辅助立方体映射来代替,它对每一个可能的输入数值包含预先估算的normalized 数值 。更多细节请看下几章的NormalMapping shader effect和ParallaxMapping shader effect。

 8
图6-11

如果你再次快速看一看源代码(或者如果你正尝试亲自编写你的第一个shader),你能发现通过屏幕输出位置的计算你入门了。因为所有的矩阵操作期待一个Vector4 ,你必须把你的Vector3输入数值转换为Vector4 ,并且把设置W成分设为1来得到worldViewProj 矩阵(转化意味着矩阵的移动)的转化部分的默认行为。

接下来的纹理坐标正好pixel shader被忽略了;你对操作它不感兴趣。你可以,举个例子,这里的乘以纹理坐标或者添加一个偏移量。对于细节映射或者water shader,带有不同的乘法因子或者偏移量,有时候复制纹理坐标,然后在pixel shader中多次应用就很有益处。

来自于apple 模型的每一个法线向量然后被转化到 world space世界空间,当你四处旋转apple 模型的时候就很重要。然后所有的法线也都被旋转,另外light看上去将不正确,因为lightDir 值不知道每一个模型被旋转了多少,并且lightDir 值正好储存在世界空间。你的顶点数据在应用于world matrix 之前,仍然是在所谓的object space中,如果你喜欢这么做(例如,四处摇晃物体或者朝一个目标方向拉伸它),它也能用于几个特效。

你在vertex shader 中最后一件要做的事是要计算light direction 和 eye vector之间的平分线向量,它帮助你在pixel shader中计算镜面反射颜色。正如我之前说过的,在这里的vertex shader 中计算它更有效率得多,而不是对于每一个点一遍又一遍再次重新计算这个值。平分线向量用于phong shading ,以及当从一个方向靠近light direction 看物体的时候,产生镜面高光(如图6-12)。

9
图6-12

Pixel Shader

pixel shader 负责最终在屏幕上输出某些像素。要测试之,你只需输出任何你喜欢的颜色;举个例子,下列代码对于每一个屏幕上被渲染的像素仅仅输出红色:

// Pixel shader
float4 PS_SpecularPerPixel(VertexOutput In) : COLOR
{
  return float4(1, 0, 0, 1);
} // PS_SpecularPerPixel(In)

如果你现在按下 Build,shader 仍然不编译,因为你还没有定义一个 technique 技法。只需定义下列 technique 技法来使得 the shader 工作。 technique的语法 总是相似的;通常你只需 one pass (这里调用的 P0 ),然后你定义使用的vertex shader和pixel shader通过具体指定应用的 vertex shader 和 pixel shader 版本:

technique SpecularPerPixel
{
  pass P0
  {
    VertexShader = compile vs_2_0 VS_SpecularPerPixel();
    PixelShader = compile ps_2_0 PS_SpecularPerPixel();
  } // pass P0
} // SpecularPerPixel

现在你终于能够在FX Composer 中编译shader ,并且你应该看到如图 6-13所示的输出。确定你在 FX Composer 的 Scene panel 分配完了shader 选项(点击 sphere,然后在 Materials panel点击SimpleShader.fx 材质,并且点击“Apply Material” 按钮)。

10
图6-13

下一步你应该进行的是把 marble.dds 纹理放到 sphere上。借助于pixel shader中的tex2D方法完成之,pixel shader期待一个纹理采样作为第一个参数,texture coordinates 作为第二个参数。用下列代码替换来自于前面代码中的return float4 这行,构造你的3D对象:

float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord);
return textureColor;

在编译shader之后,你现在应该看到如图 6-14所示的结果。如果你只看到一个黑色的 sphere 或者根本看不到 sphere,你很可能没有加载好 marble.dds 纹理(看 Textures panel 面板,确定纹理被加载就像先前描述的;你能在属性中点击diffuseTexture ,并且亲自加载它)。

11
图6-14

你必须要做的最后一件事是基于lightDir和halfVec数值,计算diffuse 散射颜色和specular镜面反射颜色成分。正如所提及的,你也想确保在pixel shader中,这些法线被重新re-normalized 来除掉人工因素。

// Pixel shader
float4 PS_SpecularPerPixel(VertexOutput In) : COLOR
{
  float4 textureColor = tex2D(DiffuseTextureSampler, In.texCoord);
  float3 normal = normalize(In.normal);
  float brightness = dot(normal, lightDir);
  float specular = pow(dot(normal, In.halfVec), shininess);
  return textureColor *
    (ambientColor +
    brightness * diffuseColor) +
    specular * specularColor;
} // PS_SpecularPerPixel(In)

diffuse color 散射颜色通过计算re-normalized的法线(在 world space, 看本章前面的 vertex shader 讨论)和 lightDir的点乘得到,lightDir也在 world space中。 如果你做任何矩阵乘法、点乘、或者叉乘计算,它们在同一空间中总是是很重要的,否则结果会大大错误。点乘 行为只是你需要计算 散射颜色的方式。 如果lightDir 和 normal point 在同一个方向,就意味着法线是正指向太阳的,并且diffuse color 应该取最大值(1.0);如果normal 是在 90 度角,点乘将返回 0 ,并且diffuse component 是 zero。为了从暗侧业能看到sphere,ambient color环境颜色被添加,当没有散射或者镜面反射光可见的时候,这种光也照亮sphere。

specular color 镜面反射颜色是由Phong公式通过法线和平分线的点乘来计算的,平分线是你在vertex shader中计算过的。然后你由 shininess 因子得到这个结果的幂,来使得被镜面高光影响的区域大大减小。越高的 shininess 值,highlight 变得越小 (如果你想目睹,就上下微调shininess 值)。你能最终在pixel shader的末尾添加所有色彩值,用纹理色彩乘以这个结果,并且返回要在屏幕上被喷绘的一切(如图 6-15)。

12
图6-15

现在你完成了shader上的工作。被保存的shader文件现在甚至能被诸如3D Studio Max 等其他程序使用,以帮助艺术家目睹3D模型在游戏引擎中将看起来如何。接下来你将在游戏引擎中执行shader。