记住,默认情况下所有函数都是内联的,因此不能递归调用。这是因为函数的处理,编译和执行都是由顶点和像素着色器
硬件来完成的。着色器硬件只能以线性方式执行代码,不能跳转到代码的其他位置。这表示函数总是被内联到调用代码中。递归将导致代码路径是不确定的,所以被禁止。
同样可以使用修饰符和关键字作为参数前缀,控制编译器对待参数的行为。表3-3列出了参数前缀和它所表示的含义。 [ uniform in out inout ] type id [ : semantic ] [ = default ] 此外,在parameter_list中定义的参数,也必须符合特定的声明语法:
表 3-3 函数参数前缀
转换类型 描述
in out inout uniform
这是默认情况下的参数行为,对函数来说,这是个只读参数。
这个前缀表示参数是一个返回值,任何对它所做的改变都将返回给调用者。 这个前缀是in和out行为的组合。
这个前缀和in前缀具有相同含义,但是特别指明参数来自于shader中的常量。
当为参数指定了语义标识符之后,他将会告诉编译器去哪里找输入数据源。举例来说,TEXCOORD0标识符将会告诉编译器把第一组纹理坐标作为这个参数的输入值。注意,标识符只对shader中的顶级函数才有意义,也就是顶点着色器或像素着色器的入口函数才能使用语义标识符。
目前为止,只剩下return_type参数没有讨论了,它用来定义函数的返回值类型。当函数没有返回值,或者通过out参数返回数据时,应该把返回值类型设置为void。
函数返回值可以是HLSL中定义的任何基本数据类型。此外,也可以是结构,允许函数同时返回一系列值。下面就是把结struct VS_OUTPUT { };
VS_OUTPUT VertexShader_Tutorial ( flaot4 inPos : POSITION ) { }
struct VS_OUTPUT { };
void VertexShader_Tutorial ( float4 inPos : POSITION,
out VS_OUTPUT outReturn)
float4 vPosition float4 vDiffuse
: POSITION; : COLOR;
使用return关键字加变量名来从函数中返回值,这里变量类型必须和函数返回值类型一致。
构作为返回值的例子:
float4 vPosition float4 vDiffuse
: POSITION; : COLOR;
VS_OUTPUT Result; //Do something… return Result;
这里,你可能在想如何使用带out前缀的参数。实际上,可以使用out参数来代替函数返回值。下面的代码展示了如何使
用out参数来代替函数返回值。
{ }
你看,在HLSL中编写函数和使用其他高级语言几乎是一样的。在学习复杂函数和shader之前,先来看看如何用函数定
义shader。
VS_OUTPUT Result; //Do something outReturn = Result;
通过函数创建Shader
编写自定义函数的主要目的之一就是通过它们定义shader。虽然讨论effect framework时我们才会详细学习如何声明shad
er,但是我希望先透露一点点内容给你。这里是使用自定义函数声明shader的例子。
flaot4 lighting ( in float3 normal, in float3 light, in float3 halfvector, in float4 color) { }
float4 myShader ( in float2 tex:TEXCOORD0, in float3 normal : TEXCOORD1,
in float3 light: TEXCOORD2, in float3 halfvector:TEXCOORD3,in float4 color:COLOR0)
PixelShader = compile ps_2_0 myShader(); { }
//compute the lighting color
Float4 lightColor = lighting(normal,light,halfvector,color); //Fetch the texture color
Float4 terColor = tex2D( texture_sampler, tex ); //Modulate the final color Return lightColor * terColro; float4 color;
color = dot ( normal, light) * color; color += dot ( light , halfvector) * color; return color;
为了让这个例子更具体一些,我们来看看如果如何编写一个简单的像素光照函数,并把它声明并编译为像素着色器。 上面的语法中,shaderProfile可以是第一章中提到过的任意一个profile值,FunctionName元素则将被编译为shader。定义
shader的过程实际上很简单,把希望的shader代码编写为一个函数,之后使用上面的语法把他编译并声明为shader。
Shader = compile shaderProfile FunctionName();
小节以及接下来的内容
这一章,我们讨论了HLSL中的函数。HLSL语言本身提供了一个丰富的内置函数库,有大约70个内建函数供开发者调
用。但更重要的是你可以通过编写自定函数定义shader,或把完成特定功能的代码打包到一起,以便复用。实际上,函数也许是HLSL中最重要的元素,每次定义shader时都必须用它。
第四章 Shader示例
上一章里,我们详细讨论了HLSL着色语言的各方面。但并没有实际展示如何编写shader。虽然本书不是关于如何编写s
hader的,但还是有必要编写几个简单的shader,帮你深入了解HLSL。此外,在学习effect framework时,我们还会用到这些例子来阐述一些核心概念。
记住,对创建一个完整的shader来说,不仅仅是编写shader代码,还包括用适当的语义符设置一系列渲染状态和变量。
当然,由于目前你还缺乏编写完整shader的一些知识,所以,这里只讨论前者:也就是顶点和像素着色程序代码。本书的后面会对这些代码进行扩展。
最简单的Shader
对于把物体渲染到屏幕上来说,有几个基本的步骤是必须完成的。首先,需要接收输入顶点的位置(顶点在世界坐标中的位置)并把它们转变为屏幕坐标。通常使用world-view-projection矩阵来完成这个任务,它包含了把顶点从局部坐标映射为最终屏幕坐标的所有信息。现在开始,我们假设已经有这样一个矩阵变量,并且名称为view_proj_matrix。
先来定义一个把数据从顶点着色器传递给像素着色器的结构。我们把这个结构称为VS_OUTPUT,当然,也可以是任何你喜欢的名字。目前,只需要把顶点位置数据添加到这个结构中。
struct VS_OUTPUT {
float4 Pos: POSITION; };
你应该注意到我们把POSITION语义连接到了Pos变量上,它将告诉effect系统如何把这个变量传递到像素着色器中。我
会在下一章讲解语义。现在只差顶点着色器代码了。顶点着色器接收顶点位置,并使用view_proj_matrix矩阵对它进行变换,可以用内建的mul函数来完成这一步计算。我们把顶点着色器代码放到一个名为vs_main的函数中:
VS_OUTPUT vs_main ( float4 inPos : POSITION) {
VS_OUTPUT Out;
// output a transformed and projected vertex position Out.Pos = mul ( view_proj_matrix , inPos); return Out; }
这里同样使用了POSITION语义修饰输入参数inPos。它将告诉顶点着色器把几何体数据流信息映射为这个参数的输入值。
接下来进入完成这个简单shader的第二步。现在你知道了顶点在屏幕上的位置,可以定义顶点的颜色了。最简单的方法就是把所有顶点的颜色都设置为一个常量。通常像素着色器将返回一个float4类型的值来表示当前像素在屏幕上的颜色值,float4分量分别表示红色,绿色,蓝色和alpha值。我们把像素着色器代码放到一个名为ps_main的函数中:
float4 ps_main ( void ) : COLOR {
//Output constant color float4 Color;
color[0] = color[3] = 1.0; // red and alpha on color[1] = color[2] = 0.0;// Green and Blue off return color; }
这几乎是最简单的代码了,注意我们用COLOR语义修饰了函数的返回值,它将告诉编译器把函数返回值作为当前像素的颜色值。
着色
我们已经有渲染物体所需的最基本代码了,如何把纹理映射到几何体上,让物体看起来更加真实呢?对于需要使用纹理的shader来说,需要有一个sampler类型的全局变量。在后面的章节中,我会教你如何使用语义和effect framework来设置纹理状态。目前我们假设已经设置了好了纹理状态:
使用纹理之前,还需要知道知道对纹理的哪一部份进行采样映射,因此,每个像素都必须有相应的纹理坐标。一般情况下,纹理坐标将作为几何体信息的一部分输入到顶点着色器中,经由顶点着色器计算处理之后,传入到像素着色器中。通常使用TEXCOORDx语义来修饰作为参数传递的纹理坐标。这个语义将会告诉硬件如何在顶点和像素着色器之间交换数据。以下是修改之后的顶点着色器代码:
struct VS_OUTPUT { }
VS_OUTPUT vs_main( { }
像素着色器也同样简单。在创建了sampler变量之后,可以使用HLSL的内建函数tex2D来对纹理进行采样,代码如下: sampler Texture0; float4 ps_main(
float4 inDiffuse : COLOR0,
float2 inTxr1 : TEXCOORD0) : COLOR0 {
//Output the color taken from our texture return tex2D ( Texture0, inTxr1); }
VS_OUTPUT Out;
//Output our transformed and projected vertex position and texture coordinate Out.Pos = mul ( view_proj_matrix, inPos); Out.Txr1 = Txr1; returen Out;
float4 inPos : POSITION; float2 Txr1 : TEXCOORD0) float4 Pos : float2 Txr1:
POSITION; TEXCOORD0;
sampler Texture0;
添加光照
虽然添加了纹理的对象看起来不错,但显然还不够真实。在增加场景真实度的过程中,很重要的一步就是为对象添加光照。真实世界中,从太阳到灯泡,充满了各种光。没有了光线,就什么都看不到了。
虽然光照本身是一个相当复杂的主题,但在计算机图形领域中,光通常被简化为几种基本类型:
? 环境光(Ambient lighting):场景中所有光源经过多次放射和折射之后,对场景总亮度贡献的近似模拟。通常用它来
减少场景中所需光源的数量,模拟出多光源下的照明效果。环境光通常是一个常量,对所有物体的作用效果都一样。 ? 漫反射光(Diffuse lighting):材质的微观粗糙表面将导致在有所方向上均匀的反射入射光线。在任何角度接收到的反
射光线强度都是相同的。
? 镜面高光( Specular lighting):当材质表面相当光滑,粗糙度很低时,将以一种非均匀的方式反射光线。对镜面高光
来说,光线强度不但与入射光角度有关,和观察者的角度也有关。
除了知道光线如何影响物体之外,你还需要如何对光源本身分类。虽然光总是由某个表面发出,比如太阳或灯泡表面,但你也可以把它们看作来自某个方向或某个点。
光照技术中,方向光是最简单的类型。它们没有位置信息,并且假设所有光线之间都是平行的,指向同一个方向。哪一种光源是这样的呢?现实中并没有这样的光源。方向光是假设光源离物体无限远时,照射到物体上的光线将近似于平行而得出的。
方向光最好的例子就是阳光。如果把太阳看作一个离地球上亿千米的点光源,那么当阳光到达地球表面时已经近似于平行了,完全可以看作是方向光。
此外没有位置信息表示方向光不随距离而衰减。对方向光来说,要考虑的因素只有两个:方向和光的颜色。看到这里你可能会问光线是如何影响物体表面的。如图所示,光线照射到物体表面的强度只与入射光线和表面法线的角度有关。
知道了这些基础知识,就可以用入射光的方向矢量和表面法线的点积以及灯光的颜色因子计算出物体表面上任意一点的光照强度和颜色。这让我们得出了以下代码:
Color = Light_Color * saturate ( dot ( Light_Direction, inNormal ) );
对这类光源来说,光线呈放射状发出。这意味着只要物体和光源的距离相等,那么无论在哪个方向,所受到的影响都相同。
由于表面的光照强度与光线和物体表面法线之间的关系有关,因此我们所要做的第一步就是计算出光线的方向。显然,对于表面上的任意点来说,光线方向就等于从当前点的位置指向光源位置的矢量。对点光源来说,随角度的衰减值如下:
//compute the normalized light direction vector and use it to determine the angular light attenuation float3 Light_Direction = normal ( inPos – Light_Position); float AngleAttn = saturate ( dot ( inNormal, Light_Direction) );
此外对于点光源来说,还需要考虑它在距离上的衰减。自然,需要计算光源到当前点的距离,使用如下代码:
float Distance = length ( inPos – Light_Position);
通常情况下,点光源的衰减因子随距离的平方成反比。但是为了获得很多的可控性,可以调整公式,让衰减和距离的二次多项式成反比,代码如下:
//compute distance based attenuation. this is defined as
// attenuatin = 1 / ( a + b*distance + c * disctance * distance)
float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.z * Dist));
现在把前面的代码集成到顶点着色器中吧:
struct VS_OUTPUT { };
float4 Light_PointDiffuse( float3 VertPos, float3 VertNorm, float3 LightPos, float4 LightColor, float4 LightAttenuation) {
float4 Pos:
POSITION;
float2 TexCoord: TEXCOORD0; float2 Color: COLOR0;
一般来说,场景中大多数的光都来自于灯泡,火炬或类似的光源。仔细观察一下这类光源,它们通常由一个很小的有限点
发出,并且位于场景中的某个特定位置。简化一下,你可以把这些光源都看作场景中的一个点,这就是点光源。
注意在上面的代码中我们使用了saturate函数。它保证对于背对光线的面来说,获得的光照强度不会为负值。当然,你也
可以使用clamp函数,但是对于把值限制在0到1之间来说,saturate函数要更加高效。