与其它高级语言一样,HLSL也包含语句和表达式。虽然这些元素通常作为函数的一部分来使用,而我们要在下一章才讲解
函数。但由于他们是构建shader的主要元素,因此,现在就来看看这些元素。
语句
语句用来控制程序流的执行顺序。HLSL定义了多种类型的语句供开发者使用。下面按功能的不同,对这些语句分为了四类:
? 表达式 ? 语句块 ? 返回语句 ? 流程控制语句
最后一项流程控制语句,它们用来控制程序的执行顺序: if ( expression ) statement [else statement ] do statement while ( expression ) while (expression) do statement
for ( [ expression | variable_decleration ] ; [ expression ] ; [exxpression ] ) statement
可以看到,HLSL中的流程控制语句与C或C++中的基本相同。但与C或C++不同的是,在shader中使用流程控制语句,必须进行一些性能上的考虑。
上面可以看出,使用return关键字和一个紧随的表达式来定义返回语句。记住,为了让程序通过编译,表达式类型必须和函数所定义的返回值类型相匹配。
接下来看返回语句。返回语句用于把函数执行的结果返回给调用者。它的语法如下: return [ expresstion] ;
简单来说,语句块就是包含在一对大括号中的语句集合。它把语句组织为一个群组,同时定义了一个子范围,块中所定义的{ [ statements ] }
变量只存在于当前语句块范围中。
接下来将详细讨论上面的每种元素,第一项是表达式,但我想把它们放到这节的后面一点,先来看看第二项,语句块。
流程控制性能考虑
目前,大多数顶点和像素着色器硬件都以线性方式执行shader,每条指令执行一次。HLSL支持的流程控制形式包括静态分
支,断言指令,静态循环,动态分支和动态循环。由于某些着色器实现的限制,部分流程控制指令可能会带来重大的性能损失。
if ( Value > 0 )
// 在r0.w中计算线性插值量 mov slt
r1.w, c2.x r0.w, c3.x, r1.w
下面是编译之后的汇编代码:
Position = Value1; Position = Value2; else
举例来说,顶点着色器1.1版的构架并不支持动态分支,因此使用if语句,产生的汇编代码将同时实现if语句中所有代码。Shader将顺序执行完这些代码,但只使用if语句中某一块代码的输出作为结果。这里是一段将使用vs_1_1编译的程序:
//根据比较结果对Value1和Value2进行插值 move mad
除像素着色器1.1以外,所有着色模型都支持流程控制语句,但只有支持顶点和像素着色器3.0的硬件才是真正使用18条流程控制语句来支持流程控制的。这意味着所有非3.0的shader都将把流程控制转换为一系列代码,执行分支的所有部分,或者把循环展开来使用。对图形硬件来说,硬件流程控制还处于起步阶段,所以性能欠佳。编写代码时应该注意如何才能让程序合理的执行。在第七章中,我将深入讨论关于动态分支的性能。
流程控制可以分为静态或动态的。对静态流程控制来说,语句块中的表达式实际上是常量,并且在shader执行前就确定了。举例来说,静态分支允许根据shader中的一个布尔常量来决定是否执行一块代码。这是一个很方便的功能,我们可以根据当前所渲染的对象类型来控制代码执行的路径。在调用渲染函数之前,你可以选择让当前shader支持哪些特性,之后,为相应代码块设置布尔标志。
相反,大部分开发者最熟悉的还是动态分支。对于动态分支来说,条件表达式的值是一个变量,只有在运行时才能确定。考虑动态分支的性能时,应该包括分支语句本身的代价以及分支中指令的代价。目前只有在硬件支持动态流程控制的顶点着色器中动态分支才是可用的。
r7, -c1
oPas, r0.w, r2, c1
add r2, r7, c0
从上面的代码可以看到,在顶点着色器模型1.1中使用if语句,将导致if语句中的所有表达式都被执行,之后通过插值来计
算最终输出。在真正支持动态分支的情况下,这个语句只会产生一条指令,但这里确需要五条指令。
除if语句外,一些硬件也允许使用动态或静态循环,但多数情况下他们都是线性执行的。
表达式
在讨论了语句之后,我们来看看表达式。表达式定义为字面值,变量或通过运算符对两者的组合。表2-6列出了所有可用的
运算符以及以及它们的含义。
表 2 – 6 运算符
运算符 用法 定义 结合
方向
() () () [] . . ++ -- ++ -- ! - + () * / % + -
(value) id(arg) type(arg) array[int] structure.id value.swizzle variable++ variable-- ++variable --variable !value -value +value (type)value value * value value / value value % value value + value value – value
子表达式 函数调用 类型构器 数组下标 选择成员 分量重组
递增后缀(作用于所有分量) 递减后缀(作用于所有分量) 递增前缀(作用于所有分量) 递减前缀(作用于所有分量) 逻辑非(作用于所有分量) 一元减法(作用于所有分量) 一元加法(作用于所有分量) 类型转换
乘法(作用于所有分量) 除法(作用于所有分量) 模运算(作用于所有分量) 加法(作用于所有分量) 减法(作用于所有分量)
左到右 左到右 左到右 左到右 左到右 左到右 左到右 左到右 右到左 右到左 右到左 右到左 右到左 右到左 左到右 左到右 左到右 左到右 左到右
< > <= >= == != && || ?: = *= /= % += -= ,
value < value value > value value <= value value >= value value == value value != value value && value value || value float ? value : value value = value variable *= variable variable /= value variable %= value variable += value variable -= value value , value
小于(作用于所有分量) 大于(作用于所有分量) 小于等于(作用于所有分量) 大于等于(作用于所有分量) 等于(作用于所有分量) 不等于(作用于所有分量) 逻辑与(作用于所有分量) 逻辑或(作用于所有分量) 或条件
赋值(作用于所有分量) 乘法赋值(作用于所有分量) 除法赋值(作用于所有分量) 取模赋值(作用于所有分量) 加法赋值(作用于所有分量) 减法赋值(作用于所有分量) 逗号
左到右 左到右 左到右 左到右 左到右 左到右 左到右 左到右 右到左 右到左 右到左 右到左 右到左 右到左 右到左 左到右
由于硬件求值方式的差异,与C语言不同,&&、||和?:三个短路求值表达式并不是短路的(译注:对多数现代语言来说,布尔表达式中,只需要部分进行求值。比如逻辑与,如果第一个表达式结果为False,则会结束这个表达式的求值,并生成一个False结果,这种方式称为短路。)。此外,你可能已经注意到了很多运算符都标注为“作用于所有分量”。这表示将对输入值(通常是4D向量)中的每一个分量进行独立运算。运算结果将保存到输出向量的相应分量中。
小结以及接下来的内容
这一章,我们学习了HLSL语言的基本内容。此时,你应该对HLSL中的数据类型有了相当了解,能定义变量,构造语句和表
达式。你看,HLSL的语法和C或C++是很类似的。
需要告诫的是,在使用高级语言编写shader时,必须时时记住目标硬件所支持的功能。特别是编写流程控制语句时。早期的继续学习,下一章,我们将讨论函数,并完成对HLSL语言部分的学习。我将教授你如何使用HLSL中丰富的预置函数库,以
硬件着色器并不支持任何形式的流程控制,因此,需要做性能上的考虑。 及如何编写你自己的函数。好了,不要浪费时间,马上进入下一章……
第三章 函数,只讨论函数
上一章里,我们学习了HLSL主要的语法元素。现在,唯一没有讲解的只剩下函数了,包括如何声明与定义函数,如何在shader中使用函数。函数是高级语言中的重要组成部分,它在shader中也同样扮演了重要角色。HLSL语法允许使用两种类型的函数。内置(或固有)函数为shader提供了一个预定义函数库,同时也为特定着色构架提供了某些特殊指令。
当然,你可以创建自定义函数。自定义函数可以用来把shader组织为一个整体,也可以用来打包部分希望重用的功能。 接下来的几节里,我将会讨论两种类型的函数,在最后还会讲解如何用函数定义shader。先来看看HLSL提供的丰富内置
函数库吧。
内置函数
HLSL着色语言包含了一系列广泛的,内置,或固有函数。这些函数在开发shader时相当有用。它们提供了从数学计算到纹理采样等广泛的功能。先依次浏览一下这些函数。
表 3-1 HLSL内置函数
函数名 用法
abs acos all
计算输入值的绝对值。 返回输入值反余弦值。 测试非0值。
any asin atan atan2 ceil clamp clip cos cosh cross ddx ddy degrees determinant distance dot exp exp2 faceforward floor fmod frac frexp fwidth isfinite isinf isnan ldexp len / lenth lerp lit log log10 log2 max min modf mul normalize pow radians reflect refract round rsqrt saturate sign sin sincos sinh smoothstep sqrt
测试输入值中的任何非零值。 返回输入值的反正弦值。 返回输入值的反正切值。 返回y/x的反正切值。
返回大于或等于输入值的最小整数。 把输入值限制在[min, max]范围内。
如果输入向量中的任何元素小于0,则丢弃当前像素。 返回输入值的余弦。 返回输入值的双曲余弦。 返回两个3D向量的叉积。 返回关于屏幕坐标x轴的偏导数。 返回关于屏幕坐标y轴的偏导数。 弧度到角度的转换 返回输入矩阵的值。 返回两个输入点间的距离。 返回两个向量的点积。
返回以e为底数,输入值为指数的指数函数值。 返回以2为底数,输入值为指数的指数函数值。 检测多边形是否位于正面。 返回小于等于x的最大整数。 返回a / b的浮点余数。 返回输入值的小数部分。 返回输入值的尾数和指数
返回 abs ( ddx (x) + abs ( ddy(x))。
如果输入值为有限值则返回true,否则返回false。 如何输入值为无限的则返回true。
如果输入值为NAN或QNAN则返回true。 frexp的逆运算,返回 x * 2 ^ exp。 返回输入向量的长度。 对输入值进行插值计算。
返回光照向量(环境光,漫反射光,镜面高光,1)。 返回以e为底的对数。 返回以10为底的对数。 返回以2为底的对数。
返回两个输入值中较大的一个。 返回两个输入值中较小的一个。 把输入值分解为整数和小数部分。 返回输入矩阵相乘的积。
返回规范化的向量,定义为 x / length(x)。 返回输入值的指定次幂。 角度到弧度的转换。
返回入射光线i对表面法线n的反射光线。
返回在入射光线i,表面法线n,折射率为eta下的折射光线v。返回最接近于输入值的整数。 返回输入值平方根的倒数。 把输入值限制到[0, 1]之间。 计算输入值的符号。 计算输入值的正弦值。 返回输入值的正弦和余弦值。 返回x的双曲正弦。
返回一个在输入值之间平稳变化的插值。 返回输入值的平方根。
step tan fanh transpose tex1D* tex2D* tex3D* texCUBE*
返回(x >= a)? 1 : 0。 返回输入值的正切值。 返回输入值的双曲线切线。 返回输入矩阵的转置。 1D纹理查询。 2D纹理查询。 3D纹理查询。 立方纹理查询。
为了贴近实际,举个例子来展示如何使用这些函数吧。假设你需要把纹理映射到一个像素上,并且使用方向光来照亮这个
像素。要完成这个任务,必须先计算光源对像素颜色的贡献,然后查找纹理颜色,最后把这两个颜色混合起来。首先,为了计算方向光的贡献,需要计算像素法线和光源方向的点积。使用dot函数可以很方便的完成这一步计算:
这个例子虽然简单,但是它展示了HLSL的强大威力,仅仅使用三行代码就能完成简单的光照。在进入下一个主题之前,
需要指出根据完成功能的不同,内建函数可以接收不同的参数。另外,由于硬件性能的不同,部分内建函数并不是在所有顶点和像素着色器版本上都可用。
最后一步,就是对灯光和像素颜色进行混合。这里,我们需要灯光颜色,灯光亮度以及像素颜色作为参数。计算很简单,FinalColor = ( LightColor * LightIntensity) * PixelColor ;
但你应该注意shader构架的矢量天性是如何对颜色中的所有分量同时起作用的。
在把灯光颜色添加到物体上之前,还需要从纹理中获得像素的颜色。假设像素已经包含了适当的纹理坐标并且有一张简单PixelColor = tex2D ( objectTexture , TextureCoord ) ;
的2D纹理,那么纹理采样的代码如下:
这里可能会出现一点小小的问题,如果像素法线背对着光源方向,那么得到的亮度将为负值。必须保证亮度在0和1之间,LightIntensity = saturate ( dot ( LightDirection , PixelNormal ) );
以避免这种效果。怎么做呢?很幸运,saturate函数能完成这个任务,修改上面的代码:
LightIntensity = dot ( LightDirection , PixelNormal);
自定义函数
你看,可以使用一系列修饰符作为函数的前缀,来控制编译器对待这些函数的行为。表 3-2列出了可能的自定义函数前缀,以及它们的含义。
接下来的是定义函数原型的语法:
[ static inline target ] [ const ] return_type id ( [ paramter_list] );
除了HLSL提供的大量内建函数以外,同样可以使用类似于C语言的方式定义自定义函数。下面就是声明函数的语法: [ static inline target] [ const ] return_type id ( [ parameter_list] ) { [statement ] }
表 3-2 自定义函数前缀
static inline
前缀 定义
这个前缀表示函数只存在于当前shader程序的作用域中,不能被多个shader共享。本书中大多数情况都不使用这个关键字。
这个前缀表示把函数代码复制到调用代码之后执行,而不是真正按照函数调用的方法来执行代码。注意,对编译器来说这个前缀只起提示作用,并不能保证函数是内联的。还需要注意,这个前缀是当前HLSL编译器的默认行为。
target const
这个前缀表示希望使用哪一个版本的顶点或像素着色器版本来编译代码。允许编译器对特定着色器版本进行优化。
这个前缀表示参数值在函数中不能改变。