利器:HLSL起步教程-完整篇

更新时间:2024-05-05 03:45:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

HLSL起步 作者:XX

目录

目录 ......................................................................................................................................................... 1

前言 ................................................................................................................................................................................. 2 1.HLSL入门 ................................................................................................................................................................. 14

1.1什么是着色器 ......................................................................................................................... 14 1.2什么是HLSL .......................................................................................................................... 15 1.3怎么写HLSL着色器 ............................................................................................................. 15

1.3.1全局变量 ...................................................................................................................... 16 1.3.2输入输出 ...................................................................................................................... 16 1.3.3入口函数 ...................................................................................................................... 16 1.3.4总结 .............................................................................................................................. 17 1.4怎么用HLSL着色器 ............................................................................................................. 17

1.4.1声明全局变量 .............................................................................................................. 17 1.4.2编译着色器 .................................................................................................................. 17 1.4.3创建着色器 .................................................................................................................. 18 1.4.4把着色器设定到渲染管道中 ...................................................................................... 18 1.4.5整个渲染过程如下 ...................................................................................................... 18

2.顶点着色器 ................................................................................................................................................................ 42

2.1可编程数据流模型 ................................................................................................................. 42 2.2顶点声明 ................................................................................................................................. 43

2.2.1 描述顶点声明 ............................................................................................................. 43 2.2.2创建顶点声明 .............................................................................................................. 44 2.2.3设置顶点声明 .............................................................................................................. 44 2.3用顶点着色器实现渐变动画 ................................................................................................. 44

2.3.1渐变动画(Morphing) .............................................................................................. 44 2.3.2渐变动画中的顶点声明 .............................................................................................. 45 2.3.3渐变动画中的顶点着色器 .......................................................................................... 45 2.3.4应用程序 ...................................................................................................................... 47 2.3.5对应用程序的一点说明 .............................................................................................. 49

3.像素着色器 ................................................................................................................................................................ 52

3.1多纹理化 ................................................................................................................................. 52 3.2多纹理效果的像素着色器 ..................................................................................................... 52

3.2.1HLSL采样器和纹理 .................................................................................................. 53 3.3应用程序 ................................................................................................................................ 54 4.HLSL Effect(效果框架) ......................................................................................................... 58

4.1Effect代码结构 .................................................................................................................... 58 4.2用Effect实现多纹理化效果 .............................................................................................. 58

4.2.1着色器 ........................................................................................................................ 58 4.2.2应用程序 .................................................................................................................... 60

结语 ....................................................................................................................................................... 64 参考资料 ............................................................................................................................................... 64

前言

本教程针对HLSL(High Level Shading Language)初学者,从应用的角度对HLSL、顶点着色器、像素着色器和Effect效果框架进行了介绍,教程中去掉了对HLSL语法等一些细节内容的讨论,力求帮助读者尽可能快地理解HLSL编程的概念,掌握HLSL编程的方法。 教程中部分阐述直接引用了其他文档,这是因为这些文档表述之精要,已经达到了不能更改的地步,这里表示感谢。 本文档版权为作者所有,非商业用途可免费使用,转载请注明出处。

一点点历史….

从1995年,3Dfx发布第一块消费级的3D硬件加速图形卡开始,计算机图形技术和相关的硬件技术都取得了重大进展。虽

然这类图形卡在渲染功能上有诸多限制,但为开发者打开了一片新的天地,终结了只能依靠软件解决方案进行渲染的时代。其结果是让实时3D图形和游戏都变得更加真实。

固定管线构架功能上的局限性,限制了开发者创建所需效果的能力。总的来说,它所产生的图形都不够真实。另一方面,

用于电影CG渲染的高端软件渲染构架则发明了一些让渲染更加逼真的方法。Pixar Animation Studios开发了一门称为RenderMan的着色语言。它的目的是让艺术家和开发者使用一门简单但强大的编程语言来完全控制渲染过程。RenderMan可以创建出高质量的图形,从照片级的真实效果,到卡通风格的非真实渲染效果都可以实现。被广泛用于当今的电影中,包括著名的动画Toy Story和A Bug’s Life。

随着处理器芯片制造技术的革新,和处理能力的增强,RenderMan的思想逐渐影响并延伸到了消费级图形硬件。DirectX

8的发布引入了顶点(vertex)和像素着色器(pixel shader)1.0 以及1.1版本。虽然这两个版本的着色模型灵活性不高,同时缺乏流程控制等一些功能。但是,这第一步,给予了艺术家和开发者长久以来所梦想的,创造夺目的、真实的图形的能力。消费级图形卡所生产的图形终于能和好莱坞电影工作室所渲染出的图形相比了。

接下来的几年间,图像硬件和3D API无论在功能和性能上都取得了巨大飞跃,甚至打破了摩尔定律中的技术进步速率。

随着DirectX 9.0 SDK以及最新的一代图形卡的发布,比如Nvidia的Geforce FX系列和ATI的Radeon 9800系列,顶点和像素着色器发展到了2.0和2.x版本。以及随后的3.x版本。

注意:

此后,接下来的几代硬件都在性能和功能方面有了重大突破。但是,由于受到硬件固定管线构架(fixed-pipeline arch

itecture)的限制,仍然有很多约束,开发者被强制只能通过使用和改变渲染状态来控制渲染过程,获得最终的输出图形。

摩尔定律是1965年,由戈登摩尔(Gordon Moore)——intel的创建者之一,通过统计得出的结论:集成电路上可容纳的晶体管数目,约每隔一年便会增加一倍。他还预测在以后的几十年中仍然将是这样。至今为止,这条理论依然很正确。另外,由于晶体管数量与集成电路的性能有关,因此,摩尔定律也是硬件性能增长的预测的依据。

这些新的着色模型为实时图像程序开发者带来了前所未有的灵活性。然而,大部分shader都通过一种低级的,类似于汇

编的语言来编写的。这意味着作为一名开发人员,你必须像多年前使用汇编语言的时代那样,自己管理寄存器,分配变量以及优化。此外,shader model 2.0和3.0增加的复杂性让开发人员更加头疼,因为不同的图形卡寄存器数量不一样,甚至同样的指令执行结果也不一样。

为了简化shader开发,同时,给予硬件开发者更多的自由优化性能,微软在DirectX 9.0中引入了High-Level Shading La

nguage(HLSL)。这门语言和其他高级语言,比如C或C++很类似,这样,开发者就能把注意力集中在shader所要实现的功能上,而不是把精力放在如何使用寄存器,或对某种硬件如何组合指令才能最优化之类的琐碎问题上。

起来。

在讲解HLSL能做什么,以及如何来使用它之前,先来看看不同的shader版本可以提供哪些功能。需要说明的是,在编

写shader之前,需要知道硬件都有哪些功能(capable)。使用HLSL并不能消除特定硬件平台上的限制,但却可以把这些限制隐藏

顶点和像素着色器管线以及Capabilities

与随DirectX 8.0发布的顶点和像素着色器1.0和1.1版本相比,shader model 2.0对语言进行了许多重要改进。由于最新

的DirectX 9.0所使用的顶点和像素着色器版本为2.0,同时,已经有大量支持vertex和pixel 2.0的显卡,所以本书主要讨论基于这一技术的shader。

注意:

虽然在编写本书时,支持shader model 3.0的图形卡已经开始上市,但尚未普及。我们会讨论一些shader model 3.0的特性,但大部分例子都是基于2.0或更低版本的shader技术。

假设你已经有一定的3D和shader基础知识,我们来看看第二代着色语言和上一代技术相比有哪些比较重要的改变。

顶点着色器2.0和2.x相对于1.x的版本有如下改进:

? 支持整数和布尔数据类型,并分别有相应的设置指令。 ? 增加了临时和常量寄存器的数量。

? 对程序所能包含的最大指令数进行了增加,给开发者以更多灵活性(标准所要求的最小指令数从128增加到了256,

某些硬件还能支持更多指令)。

? 添加了许多支持复杂运算的宏指令,比如 sine/cosine,absolute,以及power等。 ? 支持流程控制语句,比如循环和条件测试。

下面列出的则是像素着色器 2.0和2.x相对于1.x版本的改进:

? 支持扩展32-bit精度的浮点运算。

? 支持对寄存器元素的任意重组(swizzling)和遮罩(masking)。 ? 增加了常量和临时寄存器的可用数量。

? 标准所允许的最小指令卡有明显增加。算术指令从8条增加到64条,同时还允许使用32条纹理指令。像素着色器

2.x默认情况下甚至支持更多指令,允许硬件支持比标准最小要求多的指令数。 ? 支持整数和布尔常量,循环计数器以及断言寄存器(predicate register)。 ? 支持动态流程控制,包括循环和分支。

? Gradient instrctions allowing a shader to discover the dervate of any input register

通过这一系列强大的改进,如今,开发者可以自由发挥想象力,创造出令人吃惊的效果。到这里,我们应该学习一下两种着色器的构架,以便更好的了解数据是怎样在图形硬件上流动。

当渲染3D图形时,几何体信息通过Direct3D之类的渲染API传递给图形硬件。硬件一旦接收到这些信息,就为mesh中的每一个顶点调用顶点程序。图1.1描绘了顶点和像素着色器2.0标准实现的原理图。

从图1.1中可以看到,开发者通过3D渲染API,以数据流的形式,为顶点着色器提供顶点数据。数据流中包含了正确渲染几何体所需的所有信息,包括顶点位置,颜色和纹理坐标等等。当这些信息传递进来时,将分别放到合适的输入寄存器v0到v15中,以便顶点着色程序使用。顶点程序还需要访问许多其他的寄存器,才能完成自己的工作。常量寄存器都是只读的,通常用来为shader储存静态数据,因此,必须预先设置好它们的值。顶点着色器2.0标准下,常量寄存器储存的都是矢量,可以保存浮点数,整数,以及布尔类型的值。需要注意,顶点着色器中所有的寄存器都把数据储存为包含4个分量的矢量,可以并行访问所有分量,也可以使用重组或遮罩分别访问某个分量。

在图1.1的右边部分,是临时寄存器,用来储存顶点着色器计算出的中间结果。显然,由于它们是临时性的,因此,可以对这些寄存器进行写入和读取操作。注意名称为a0到aL的寄存器,它们是循环时用来索引地址和追踪循环所用的计数寄存器(counter register)。记住,由于HLSL是一门高级的着色语言,你不需要关心寄存器是如何分配的。对开发者来说这个过程应该是透明的,并且只有在shader被最终编译为机器代码时才发生。

在访问了输入寄存器,临时寄存器和常量寄存器之后,顶点着色器程序才开始以开发者所希望的方式来处理和控制输入的顶点。处理过程结束之后,结果立即被输送到最终的输出寄存器中。其中

词汇约定

虽然语法定义了如何把所有语言元素组合到一起,比如,如何定义函数和代码片段,但它只定义了如何把表达式和操作

符以及标识符一起使用。这意味着语言的语法并没有定义文法(例如: 什么是标识符)。接下来的几段详细解释了HLSL编译器中的词汇约定。

空白字符

HLSL语言中,下面这些字符都被认为是空白字符: ? 空格 ? Tab字符 ? 换行符

? C风格的注解(/* */)。 ? C++风格的注解( // )。

? asm代码块中,汇编风格的注解(;)。

数字

HLSL中的数字可以为浮点类型,也可以为整型。浮点数通常有以下呈现形式:

Float:

( fractional-constant [ exponent-part ] [ float-suffix ] )| (digit-sequence exponent-part [ float-suffix ] )

( [digit – sequence] . digit – sequence ) | (digit – squence . )

Fractnal-const: Sign:

+ | -

Dight – sequence: digit | (digit – sequence digit )

floating – suffix: h | H | f | F

整型的语法与此类似:

Integer: integer – constant [ interger – suffix ] Integer – constant:

digit – sequence | ( 0 digit – sequence ) | ( 0 × digit – sequence )

Digit – sequence: digit | ( digit – sequence digit ) Integer – suffix: u | U | l | L

字符

HLSL允许定义字符和字符串。字符串都是由字符组成。下面是字符的定义:

? ‘ c ‘ (字符)

? ‘ \\t ‘ , ‘ \\n ‘,…..( 转义字符 ) ? ‘ \\### ‘ (八进制换码顺序 ) ? ‘ \\x## ‘ (十六进制换码顺序)

注意

预处理指令中不能包含转义字符。

字符串包含在一对引号中,可以包含前面所述的任意有效字符组合。

标识符

标识符用来表示函数名或变量名之类的语言元素。除了前面所列的关键字以外,标识符可以是字母和数字的任意组合,

但必须保证第一个字符为字母。

操作符

HLSL定义了一组操作符,以便在表达式中使用。表1-5 列出了所有标准操作符,以及他们的含义。如果你熟悉

C或C++,那么这些操作符对你来说应该是一目了然的。

表 1-5 HLSL 操作符

操作符 描述

++ -- && || == :: << <<= >> >>= … <= >= != *= /= += -= %= &= |= ^= ->

一元加法。 一元减法。 逻辑与。 逻辑或。 等号。

成员标识符(用于结构和类)。 二进制左移。

自赋值(self assigning)二进制左移,a<<= b等于a = a<

自赋值二进制右移,a>>=b等于a = a>>b。 省略符(用于可变参数函数)。 小于等于。 大于等于。 不等于。

自赋值乘法, a *= b 等于 a = a * b。 自赋值除法, a /= b 等于 a = a / b。 自赋值加法, a += b 等于 a = a + b。 自赋值减法, a -= b 等于 a = a – b。 自赋值求余, a %= b 等于 a = a % b。 自赋值逻辑与, a &= b 等于a = a & b。 自赋值逻辑或, a |= b 等于 a = a | b。 自赋值求幂, a ^= b 等于 a = a ^ b。 重定向操作符,用来访问结构成员。

语言语法

HLSL语言的语法相当简单。初看可能有些复杂,但只要使用它写几个程序,你马上就能掌握要领。目前为止,你不应

该对语法太过担心,后面的章节我们将逐步了解语言的每个部分。由于实际的语法表相当长,我决定单独把他放到附录D中。另外你也可以参考DirectX SDK获取更多信息。

(译注:请参考DirectX SDK中DirectX Graphics--Reference--HLSL Shader Reference--Appendix中的Language Syntax部分)

观察表的第一行,可以看到HLSL程序被定义为一个program。每个program要么为空,要么包含一系列decl(声明)。

最初的两行表示每个decls可以由多条其它decl组成。你可能已经注意到,声明可以用来定义空白语句,类型声明,变量声明,结构声明,函数声明或technique声明。语义定义了不同声明类型等等。

小结以及接下来的内容

在这一章里,我们简要概括了DirectX和shader技术在过去几年间的发展和历史。随着shader model 2.0和3.0复杂度的增加,开发者不但需要利用语言的所有新能力,同时,还需要高效的完成任务。由于新着色管道的指令和通用处理器上的指令越来越类似,因此,开发一门高级语言,让开发者把注意力集中在shader所要实现的功能上,而不是把精力放在如何使用寄存器,或对某种硬件如何组合指令才能最优化之类的琐碎问题上是很有意义的。

在需求的驱动下,微软开发并通过DirectX SDK发布了HLSL着色语言,帮助开发者使用最新的图形技术,创建更加真实的图形。本章,我们学习了很多语法背后的基础知识。虽然这章看起来有些枯燥,不要担心,随后的几个章节我们就会讨论一些比较有趣的内容。

1.HLSL入门

1.1什么是着色器

DirectX使用管道技术(pipeline)进行图形渲染,其构架如下:

图1.1 Direct3D Graphics Pipeline

之前我们使用管道的步骤如下:

1. 设定顶点、图元、纹理等数据信息; 2. 设定管道状态信息;

? 渲染状态

通过SetRenderState方法设定渲染状态; 另外,使用以下方法设置变换、材质和光照:

SetTransform SetMaterial SetLight

LightEnable ? 取样器状态

通过SetSamplerState方法设定取样器状态; ? 纹理层状态

通过SetTextureStageState设定纹理层状态;

3. 渲染;

这部分交由D3D管道按照之前的设定自行完成,这部分操作是D3D预先固定的,所以这种管道技术被称为固定功能管道(fixed function pipeline);

固定功能管道给我们编程提供了一定的灵活性,但是仍有很多效果难以通过这种方式实现,比如:

1. 在渲染过程中,我们要求y坐标值大于10的顶点要被绘制到坐标值(0,0,0)的地方,在之前的固定功能管

道中,顶点被绘制的位置是在第1步即被设定好的,不可能在渲染过程中进行改变,所以是不可行的;

2. 某顶点在纹理贴图1上映射为点A,在纹理贴图2上映射为点B,我们要求该顶点颜色由A、B共同决定,即:

定点颜色 = A点色彩值*0.7 + B点色彩值*0.3

这在固定管道编程中也是不可行的。

以上两个问题都可以由可编程管道(pragrammable pipeline)来解决。

可编程管线允许用户自定义一段可以在GPU上执行的程序,代替固定管道技术中的Vertex Processing和Pixel Processing阶段(参照图1.1),从而在使我们在编程中达到更大的灵活性。其中替换Vertex Processing的部分叫做Vertex Shader(顶点着色器),替换Pixel Proccessing的部分叫做Pixel Shader(像素着色器),这就是我们所说的着色器Shader。

1.2什么是HLSL

Direct8.x中,着色器是通过低级着色汇编语言来编写的,这样的程序更像是汇编式的指令集合,由于其效率低、可读性差、版本限制等缺点,迫切要求出现一门更高级的着色语言。到了Direct3D9,HLSL(High Level Shading Language,高级渲染语言)应运而生了。

HLSL的语法非常类似于C和C++,学习起来是很方便的。

1.3怎么写HLSL着色器

我们可以直接把HLSL着色器代码作为一长串字符串编写进我们的应用程序源文件中,但是,更加方便和模块化的方法是把着色器的代码从应用程序代码中分离出来。因此,我们将着色器代码单独保存为文本格式,然后在应用程序中使用特定函数将其加载进来。

下面是一个完整的HLSL着色器程序代码,我们把它保存在BasicHLSL.txt中。该着色器完成顶点的世界变换、观察变换和投影变幻,并将顶点颜色设定为指定的颜色。

//

// BasicHLSL.txt // //

// Global variable //

matrix WVPMatrix; vector color; //

// Structures //

struct VS_INPUT { vector position : POSITION; };

struct VS_OUTPUT { vector position : POSITION; vector color : COLOR; }; //

// Functions //

VS_OUTPUT SetColor(VS_INPUT input) { VS_OUTPUT output = (VS_OUTPUT)0; output.position = mul(input.position, WVPMatrix); output.color = color;

return output; }

下面就针对上述代码讲解一下HLSL着色器程序的编写:

1.3.1全局变量

代码中声明了两个全局变量:

matrix WVPMatrix; vector color;

变量WVPMatrix是一个矩阵类型,它包含了世界、观察、投影的合矩阵,用于对顶点进行坐标变换; 变量color是一个向量类型,它用于设定顶点颜色; 代码中并没有对全局变量进行初始化,这是因为我们对全局变量的初始化过程将在应用程序中进行,全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在。具体赋值过程将在后续部分讲述。

1.3.2输入输出

? 输入输出结构

程序中定义了两个输入输出结构VS_INPUT和VS_OUTPUT

struct VS_INPUT { vector position : POSITION; };

struct VS_OUTPUT { vector position : POSITION; vector color : COLOR; };

自定义的结构可以采用任意名称,结构不过是一种组织数据的方式,并不是强制的,你也可以不使用,而将本程序的输入改为:

vector position : POSITION; ? 标志符

用于输入输出的变量采用用一种特殊的声明方式:

Type VariableName : Semantic

这个特殊的冒号语法表示一个语义,冒号后面的标志符用来指定变量的用途,如

vector position : POSITION;

其中,POSITION标志符表明该变量表示顶点位置,另外还有诸如COLOR、NORMAL等很多表示其他意义的标志符。 本节所说的输入输出其实是指着色器代码和编译器、GPU之间的通信,和应用程序是无关的,所以这些变量不需要在应用程序中进行赋值,标志符告诉编译器各个输入输出变量的用途(顶点位置、法线、颜色等),这是着色器代码和编译器、GPU之间通信的关键。

1.3.3入口函数

程序中还定义了一个函数SetColor:

OUTPUT SetColor(INPUT input) { VS_OUTPUT output = (VS_OUTPUT)0; output.position = mul(input.position, WVPMatrix); output.color = color; return output; }

1. 该函数以input和output类型作为输入输出;

2. 使全局变量WVPMatrix和input.position相乘,以完成顶点的世界、观察、投影变换,并把结果赋值到

output.position; output.position = mul(input.position, WVPMatrix);

3. 将全局变量color的值赋给output.color;

output.color = color;

4. 在同一个着色器代码文件中,可以有多个用户自定义函数,因此在应用程序中需要指定一个入口函数,相当于

windows程序的WinMain函数,本程序只包含SetColor一个函数而且它将被做为入口函数使用。

1.3.4总结

至此,一个HLSL着色器编写完毕,渲染过程中,当一个顶点被送到着色器时:

1. 全局变量WVPMatrix、color将在应用程序中被赋值;

2. 入口函数SetColor被调用编译器根据标志符将顶点信息填充到VS_INPUT中的各个字段;

3. SetColor函数中,首先定义一个VS_OUTPUT信息,之后根据WVPMatrix和color变量完成顶点的坐标

变换和颜色设定操作,最后函数返回VS_OUTPUT结构;

4. 编译器将会再次根据标志符把返回的VS_OUTPUT结构中的各字段映射为顶点相应的信息。 5. 顶点被送往下一个流程接受进一步处理。

上述过程中,全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在;标志符告诉编译器各个输入输出变量的用途(顶点位置、法线、颜色等),这是着色器代码和编译器、GPU之间通信的关键。个人认为这是着色器中最为精义的地方:)

1.4怎么用HLSL着色器

应用程序中对HLSL着色器的使用分为以下步骤: 1. 加载(称为编译更为妥当)着色器代码; 2. 创建(顶点/像素)着色器;

3. 对着色器中的变量进行赋值,完成应用程序和着色器之间的通信。 4. 把着色器设定到渲染管道中;

本例使用的着色器是一个顶点着色器,因此我们将通过顶点着色器的使用来讲解着色器的使用过程,像素着色器的使用过程与此大同小异,二者之间仅有些微差别。

1.4.1声明全局变量

IDirect3DVertexShader9* BasicShader = 0; //顶点着色器指针

ID3DXConstantTable* BasicConstTable = 0; //常量表指针

D3DXHANDLE WVPMatrixHandle = 0; D3DXHANDLE ColorHandle = 0;

ID3DXMesh* Teapot = 0; //指向程序中D3D茶壶模型的指针

1.4.2编译着色器

通过D3DXCompileShaderFromFile函数从应用程序外部的文本文件BasicHLSL.txt中编译一个着色器:

//编译后的着色器代码将被放在一个buffer中,可以通过ID3DXBuffer接口对其进行访问,之后的着色器将从这里创建

ID3DXBuffer* shaderBuffer = 0; //用于接受错误信息

ID3DXBuffer* errorBuffer = 0; //编译着色器代码

D3DXCompileShaderFromFile(\着色器代码文件名

0, 0,

\入口函数名称 \顶点着色器版本号

D3DXSHADER_DEBUG,// Debug模式编译 &shaderBuffer, //指向编译后的着色器代码的指针 &errorBuffer,

&BasicConstTable); //常量表指针

1.4.3创建着色器

应用程序通过CreateVertexShader创建一个顶点着色器,注意使用了上一步得到的shaderBuffer:

g_pd3dDevice->CreateVertexShader((DWORD*)shaderBuffer->GetBufferPointer(), &BasicShader); 1.4.3对着色器中的变量进行赋值

1.3.4节说到着色器的全局变量在应用程序中赋值而在着色器程序中使用,这是应用程序和着色器通信的关键所在,这里就具体说明赋值过程。

着色器中的全局变量在编译后都被放在一个叫常量表的结构中,我们可以使用ID3DXConstantTable接口对其进行访问,参照1.4.1中编译着色器函数D3DXCompileShaderFromFile的最后一个参数,该参数即返回了指向常量表的指针。

对一个着色器中变量进行赋值的步骤如下:

1. 通过变量名称得到指向着色器变量的句柄;

还记得在BasicHLSL.x着色器文件中我们声明的两个全局变量吗:

matrix WVPMatrix; vector color;

我们在应用程序中相应的声明两个句柄:

D3DXHANDLE WVPMatrixHandle = 0; D3DXHANDLE ColorHandle = 0; 然后通过变量名得到分别得到对应的两个句柄:

WVPMatrixHandle = BasicConstTable->GetConstantByName(0, \ ColorHandle = BasicConstTable->GetConstantByName(0, \2. 通过句柄对着色器变量进行赋值;

我们可以先设置各变量为默认值:

BasicConstTable->SetDefaults(g_pd3dDevice);

之后,可以使用ID3DXConstantTable::SetXXX函数对各个变量进行赋值:

HRESULT SetXXX(

LPDIRECT3DDEVICE9 pDevice, D3DXHANDLE hConstant, XXX value );

其中XXX代表变量类型,例如Matrix类型的变量就要使用SetMatrix函数赋值,而Vector类型的则要使用SetVector来赋值。

1.4.4把着色器设定到渲染管道中

这里我们使用SetVertexShader方法把顶点着色器设定到渲染管道中:

g_pd3dDevice->SetVertexShader(BasicShader);

1.4.5整个渲染过程如下

在渲染过程中,我们设定顶点的变换坐标和颜色值,渲染代码如下:

g_pd3dDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,

D3DCOLOR_XRGB(153,153,153), 1.0f, 0 ); //开始渲染

g_pd3dDevice->BeginScene();

//得到世界矩阵、观察矩阵和投影矩阵

D3DXMATRIX matWorld, matView, matProj;

g_pd3dDevice->GetTransform(D3DTS_WORLD, &matWorld); g_pd3dDevice->GetTransform(D3DTS_VIEW, &matView);

g_pd3dDevice->GetTransform(D3DTS_PROJECTION, &matProj); D3DXMATRIX matWVP = matWorld * matView * matProj;

//通过句柄对着色器中的WVPMatrix变量进行赋值

BasicConstTable->SetMatrix(g_pd3dDevice, WVPMatrixHandle, &matWVP);

D3DXVECTOR4 color(1.0f, 1.0f, 0.0f, 1.0f);

//通过句柄对着色器中的color变量进行赋值,这里我们赋值为黄色

BasicConstTable->SetVector(g_pd3dDevice, ColorHandle, &color);

//把顶点着色器设定到渲染管道中

g_pd3dDevice->SetVertexShader(BasicShader);

//绘制模型子集

Teapot->DrawSubset(0);

//渲染完毕

g_pd3dDevice->EndScene();

g_pd3dDevice->Present(NULL, NULL, NULL, NULL);

编译运行程序,运行效果如图1.2所示,这里我们将顶点颜色设置为黄色,如果读者在渲染过程中不断变换对着色器变量color的赋值,你将会得到一个色彩不断变幻的D3D茶壶。

D3DXVECTOR4 color(1.0f, 1.0f, 0.0f, 1.0f); //读者可以尝试改变颜色值

BasicConstTable->SetVector(g_pd3dDevice, ColorHandle, &color);

图1.2 着色器效果

第二章 HLSL着色语言

这一章我们将学习HLSL的大部分基本内容,包括丰富的变量类型,如何定义变量,和如何编写shader代码。你可能还

想知道如何在shader中使用函数。虽然它们是使用HLSL编写shader的必须元素,但我还是决定把所有关于函数的内容作为单独一章来讲解。在第三章中,我们将详细讲解如何定义函数,以及HLSL为开发者所编写的内置函数。

好了,闲谈到此为止,让我们直接进入本章主题,讨论在任何语言中都是最重要的元素,数据类型。

在继续学习之前,需要进行一点点说明,读者在前一章学习HLSL语法时可能注意到了诸如techniques,渲染状态,pas

s之类的概念。虽然它们也是HLSL语言的一部分,但事实上它们只在effect文件中使用,而effect 文件则是HLSL的一个超集。

数据类型

数据类型是任何一门编程语言的核心。没有它们你将无法定义变量或者在函数之间传递数据。如果连呈现数据的方法都没有,又怎么能储存数据呢?和其他高级语言一样,HLSL有一系列内置定义类型,可以把它们分为以下几类:

? 标量类型 ? 矢量类型 ? 矩阵类型 ? 对象类型

除了上面的内置类型外,HLSL同样允许自定义的复合类型,比如数组和结构。在介绍HLSL特定数据类型前,先来仔细看看大部分3D硬件上呈现数据的方式。当处理数据时,基本的选项是浮点值,根据硬件构架的不同,这些值可能为16或32位。记住,虽然可以让硬件同一时间只处理一个值,但这并不是最高效的方法。相反,如果硬件每次处理一个包含四个分量值的单元(或矢量),那么则可以同时对四个分量进行处理。当然,这就需要考虑硬件如何来保存非矢量的值。我稍后会讲解这一点。

除了浮点值以外,较新的硬件也内置支持整型和布尔值,但它们主要用于分支,循环,和条件测试语句。接下来的几页中,我将浏览每一种数据类型,并解释如何来使用它们。

标量类型

标量类型是由HLSL标准定义的,他们是最基本的类型。所有复杂类型,比如矢量,矩阵,和结构都由标量组成。表2-1列出了所有可用的标量类型以及它们所表示的值。

表 2 -1 HLSL标量类型 标量类型 值

bool int half float double

true或false 32位有符号的整型 16位浮点数 32位浮点数 64位浮点数

需要注意并不是所有shader目标都天生支持整型,half和double值。如果要将shader编译为不支持指定类型数据的目标,那么将模拟处理float值的方式来处理它们。与使用原生(native)类型相比,结果可能不正确。在确定目标平台支持某个特定类型之前,为了保证一致性和可移植性,最好坚持使用标准浮点数。

注意

我们在上一章已经简要讨论过profile,它用来告诉HLSL编译器为哪种构架的shader产生代码。通常Profile的值直接对应于可用的顶点和像素着色器版本,但对于指令集没有改变的情况可能会有例外。

矢量和矩阵类型

矢量和矩阵是把标量组织为1维或2为数组的标准方式。它们通常用来表示3D数据,比如法线和变换矩阵。

矢量是HLSL标准所定义的,由特定标量类型组成的一维数组。默认情况下,一个矢量由4个浮点值组成。如表2-2所示,可以手动定义任何类型的矢量。

表 2 -2 HLSL矢量类型 矢量类型 值

vector

vector

一个包含四个浮点分量的矢量 包含size个type类型的矢量

另外,为了简便,HLSL语言预定义了一组标准矢量类型,方便开发者使用。下面就是这些预定义的矢量类型

typedef vector VECTOR; typedef vector bool# ; typedef vector int# ; typedef vector half# ; typedef vector float# ; typedef vector double# ; (译注:“#”表示分量个数。)

你可能还记得我之前提到过图形硬件是基于矢量的。你看,这里所定义的矢量数据类型恰好与硬件相对应,编译器将把这些数据直接映射为矢量。当你使用少于四个分量的矢量时,编译器将在空闲分量中添加其他变量,以便压缩数据。

简要讨论了矢量之后,你可能想知道如何才能单独访问矢量中的某个分量。有许多方式允许我们单独访问矢量中的分量。下面列出了访问数组分量的几种不同方式。注意对多于四个分量的数组来说,额外的分量只能通过索引访问。

? 使用分量符访问:vector.x , vector.y , vector.z , vector.w ? 使用颜色符访问:vector.r , vector.g , vector.b , vector.a ? 使用索引访问:vector[0], vector[1], vector[2], vector[3]

接下来看看矩阵类型。矩阵是HLSL标准所定义的,由特定标量类型组成的二维数组。默认情况下

分量访问和重组

如前所述,可以像访问结构成员一样来访问矢量和矩阵中的单个分量。这里对矢量和矩阵的访问方式做个小结。

注意,所有下标都必须来自于同一下标集,(比如xyzw,rgba,或者_11到_44),不同下标集之间的元素不能进行混

合,比如不能出现xyrg这样的组合。同一个分量可以出现多次。但对赋值目标来说,则不允许分量重复出现。

tempMatrix._m00_11 = worldMatrix._m00_m11; tempmatrix._11_22_33 = worldMatrix._24_23_22; temp = fMatrix._m00_m11; temp = fMatrix._11_22; 对矩阵来说也是如此。 bgr, yyzw, _12_22_32_42

此外,通过把二到四个分量名串连到一起作为下标来使用,还可以让矢量只包含某几个指定分量,HLSL把这门技术称

为重组。这里就是一些有效的重组实例:

_11, x, r _21 _31 _41

_12, y, g _22 _32 _42

_13, z, b _23 _33 _43

_14, w, a _24 _34 _44

注意

并不是所有硬件都支持所有类型的重组操作。对像素着色器1.x的某些版本来说,部分重组操作是不允许的。编译器可以通过模拟来补偿这些限制,但将带来一定性能损失。

对象类型

? 采样器 ? 纹理 ? 结构体

? 顶点和像素着色器 ? 字符串

字符串指使用ASCII码定义的字符串,它们除了用作注解以外,用处不是太多,这里也不对它进行详细讨论。采样器,

着色器和纹理才是我们所关心的重点部分。

结构的内容将在下一节关于自定义类型的部分详细讨论。

HLSL还定义了一系列范围广泛的对象数据类型。这些类型通常用来呈现非数字数据与复合类型的句柄,典型的例子就

是纹理和结构体。以下是HLSL中所定义的对象类型:

采样器和纹理

HLSL定义了两种数据类型用于在shader中获取纹理信息。纹理本质上就是一个指向硬盘上一系列物理像素信息的句柄。

采样器则是一组纹理采样参数的组合,比如warp mode或者mipmap属性等。

顶点和像素着色器

HLSL定义了两种数据类型用来保存顶点和像素着色器,它们分别是vertexshader和pixelshader。如果使用asm关键字以汇编方式来编写着色程序,则可以把这些程序直接分配给着色器:

vertexshader vs = {

vs_2_0 };

如果使用HLSL,那么则必须通过函数来定义着色器。下面就是使用高级指令定义像素着色器的例子: pixelshader ps = compile ps_2_0 psmain(); decl_position v0 mov oPos, v0

结构及自定义类型

除了前面看到的大量预定义数据类型以外,HLSL也允许开发者创建新类型。自定义类型通常都是结构,它们是由一组

其他(内置或自定义)数据类型,或基于现有类型声明的新类型,组成的对象。

使用关键字struct来定义结构。结构是复合类型,用来把多个数据组合为一个整体。用下面的语法来定义结构: struct [ ID ] { members }

这里是创建结构的一个例子:

struct Circle { };

数组后缀可以跟随在ID之后,允许把数组作为新类型。当声明了一个类型之后,就可以通过ID来对它进行引用。注意array_suffix由一个或多个literal_integer_expression组成,用来表示数组维度。

另外,HLSL还允许使用typedef关键字,为现有类型声明一个新名称。它的语法如下: typedef [ const ] type id [ array_suffix ] [ , id…];

float4 Position; float Radius;

类型转换

在程序设计中,术语类型转换表示把一种数据转变为另一种数据的能力。HLSL支持多种内置类型间的转换。表2-4总结

了内置数据间可能的转换。

表 2 -4 HLSL中的类型转换

转换类型 描述

标量——标量 标量——矢量 标量——矩阵 标量——对象 标量——结构 矢量——标量 矢量——矢量 矢量——矩阵 矢量——对象 矢量——结构

这类转换总是有效的。当把布尔值转换为整型或浮点类型时,false值表示0,true表示1。同样,当把整型或浮点类型转换为布尔值时,0表示false。当把浮点类型转换为整型时,将四舍五入为最接近的整数。 这类转换总是有效的。转换将把标量复制并填充到矢量中。 这类转换总是有效的。转换将把标量复制并填充到矩阵中。 这类转换是是无效的。

这类转换仅当结构中的成员都为数字时才是有效的。转换将把标量复制并填充到结构中。 这类转换总是有效的。转换将复制矢量中的第一个分量,并填充到标量中。

目标矢量容量不大于源矢量时才是有效的。转换只保留最左边(left-most)的分量,截去剩下的分量。 只有当矢量和矩阵一样大时,转换才是有效的。 这类转换总是无效的。

这类转换只有当结构容量不大于矢量,且所有成员都为数字时才是有效的。

矩阵——标量 矩阵——矢量 矩阵——矩阵 矩阵——对象 矩阵——结构 对象——标量 对象——矢量 对象——矩阵 对象——对象 对象——结构 结构——标量 结构——矢量 结构——矩阵 结构——结构

这类转换总是有效的。转换将把矩阵左上角的值填充到标量中。 只有当矢量和矩阵一样大时转换才是有效的。

只有当目标矩阵维度不大于源矩阵时,转换才是有效的。转换将把源矩阵填充到目标矩阵的左上部分,并且丢弃余下数据。 这类转换总是无效的。

只有当结构容量和矩阵一样大,且所有成员都为数字时,转换才是有效的。 这类转换总是无效的。 这类转换总是无效的。 这类转换总是无效的。

只有当两个对象都是同一类型时,转换才是有效的。

只有当结构包含一个以上的成员时,转换才是有效的。结构中成员的类型必须和对象的类型一样。 只有当结构包含一个以上的成员时,转换才是有效的。这个成员必须为数字。

只有当结构容量不小于矢量时,转换才是有效的。它的第一个成员必须为数字,并且等于矢量的大小。 只有当结构容量不小于矩阵时,转换才是有效的。它的第一个成员必须是数字,并且等于矩阵大小。 只有当目标结构容量不大于源结构容量时,转换才是有效的。目标结构和源结构间各自成员的转换也必须是有效的。

定义变量

从语法定义规范可以看出,对变量可以使用多个变量关键字前缀,来告诉编译器如何对待变量。表2-5列出了不同前缀所表示的含义。

HLSL允许把变量定义为常量,输入,输出和临时变量。变量的标准定义语法如下: [static uniform vloatile extern shared ] [ const ] type id [ array_suffix ] [ :semantics ] [ = initializers ] [ annotations ] [ , id …];

表 2 – 5 变量前缀

前缀 描述

static uniform extern volatile shared const

static用于全局变量,表示值为一个内部变量,仅能被当前shader中的代码访问。用于局部变量则表示这个值将在多次调用间驻留。静态变量只能进行一次初始化操作,如果没有使用特定值进行初始化,则默认为0。 使用uniform声明的全局变量表示对整个shader来说,它是一个统一的输入值,即不能在shader运行期间改变。所有非静态全局变量都被认为是统一的。

使用extern声明的全局变量表示对shader来说,它是一个外部输入的值。所有非静态全局变量都被认为是外部的。

这个关键字提示编译器变量将频繁改变。

这个关键字用来修饰非全局变量,告诉编译器这个值将被多个effect共享。 声明为const的变量表示在初始化之后,就不能改变它的值。

声明变量的语法中,需要注意的是semantics部分。语义(semantixs)对HLSL语言来说并没有什么特别含义,它是用来定义

如何把shader中的变量及变量的含义映射到effect framewrok中的。

语义通常用于顶点和像素程序的输入和输出变量,把他们映射为特定含义,比如顶点位置或纹理坐标。举例来说,COLOR0

语义用来告诉编译器,指定变量表示第一种漫反射颜色,并且在大多数shader版本中,将把它放到d0寄存器中。

如语法定义规范所示,允许全局变量包含一个注解(annotation)。注解以 { member_list }的形式出现。Member_list是一个

程序声明的集合,其中每个成员都被初始化为指定字面值。注解只能用来为effect传递元数据,不能在程序中对它进行引用。

我们将在第六章详细讨论语义和注解。

语句和表达式

与其它高级语言一样,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

这个前缀表示希望使用哪一个版本的顶点或像素着色器版本来编译代码。允许编译器对特定着色器版本进行优化。

这个前缀表示参数值在函数中不能改变。

记住,默认情况下所有函数都是内联的,因此不能递归调用。这是因为函数的处理,编译和执行都是由顶点和像素着色器

硬件来完成的。着色器硬件只能以线性方式执行代码,不能跳转到代码的其他位置。这表示函数总是被内联到调用代码中。递归将导致代码路径是不确定的,所以被禁止。

同样可以使用修饰符和关键字作为参数前缀,控制编译器对待参数的行为。表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函数要更加高效。

}

//determine the distance from the light o the vertex and the direction float3 LightDir = LightPos – VertPos; float Dist = length(LightDir); LightDir = LightDir / Dist;

//Compute distance based attenuation.

float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.y * Dist*Dist)); //comopute angle based attenuation

float AngleAttn = saturate ( dot (VertNorm, LightDir));

// Computer the final lighting

return LightColor * DistAttn * AngleAttn;

VS_OUTPUT vs_main( float4 inPos: POSITION, { }

我把计算点光源光照的代码单独放到了Light_PointDiffuse函数中,因此,当场景中有多个点光源时,你可以复用这段代码。当然,我们在后面的章节会有这样的例子。

我们已经有了个一个漫反射点光源着色器,现在应该考虑光照方程中的高光部分了。和漫反射相比,镜面高光最大的区别就是光照亮度不但与光线到表面的角度有关,还和观察者的角度有关。

VS_OUTPUT Out; Out.TexCoord = inTxr;

float3 inNormal: NORMAL, float2 inTxr : TEXCOORD0)

Out.Pos = mul ( view_proj_matrix, inPos);

float4 Color = Light_PointDiffuse ( inPos, inNormal, Light_Position, Light1_Color, Light_Attenuation) Out.Color = Color;

return Our;

为了计算高光,我们需要一个称为中间(half)矢量的值。这个矢量其实是观察矢量和灯光矢量的中间值。为了计算观察矢量需要把观察点变换到模型空间。我们假设观察点的位置位于( 0,0,10)。把它变换到模型空间之后,加上顶点位置,就是最终的观察矢量:

HalfVect = normalize ( LightDir – EyeVetor);

把EyeVector与光源矢量混合到一起,然后进行标准化,就是中间矢量。由于EyeVector和LightDir都是标准矢量,所以

中间矢量相当于二者的均值(A + B)/ 2。

EyeVector = -normal(mul ( inv_view_matrix, float4(0,0,10,1) + inPos);

(译注:更直观的做法是在世界坐标下计算EyeVector,然后用LightDir + EyeVetor计算HalfVect)

有了中间矢量,把它和表面法线的点积进行m次幂运算,就能算出光源基于角度的光线强度。幂运算时的指数相当于物体的镜面指数,值越大,高光区域就越小也越明亮。下面的代码假设镜面指数为32。

struct VS_OUTPUT { };

float4 Light_PointSpecular( float3 VertPos, float3 VertNorm,float3 LightPos, { }

VS_OUTPUT vs_main( float4 inPos:POSITION,float3 inNormal:NORMAL,float2 inTxr:TEXCOORD0) { }

当考虑光照时,大部分人都认为逐顶点的光照足够好了。对于镶嵌度较高的模型来说是这样,但对某些复杂或的精度模型当逐顶点照亮对象时,将为每个顶点计算一次光照颜色,然后在通过顶点在多边形所覆盖的区域对像素颜色进行线形插值。

来说却不一定。出现这种效果的原因是顶点间颜色插值的方式。

现实中,光照值取决于光线角度,表面法线,和观察点(对于镜面高光来说)。与逐像素对所有光照元素进行单独插值,再计算光照相比,直接对顶点颜色进行插值所得的结果通常不够精确,特别是对面积较大的多边形来说。

VS_OUTPUT Out;

//compute the projected positon and send out the texture coordinates Out.Pos = mul(View_proj_matrix,inPos); Out.TexCoord = inTxr; //Output the ambient Color float4 Color = Light_Ambient; //Determine the eye vector

float3 EyeVector = -normlaize(mul(inv_view_matrix,float4(0,0,10,1)) + inPos); //computer the light contribution

Color = Light_PointSpecular(inPos,inNormal,Light1_Position,Light1_Color,Lihgt_Attenuation, EyeVector); //Output Final Color Out.Color = Color; return Out

float4 Pos: POSITION;

float2 texCoord: TEXCOORD0; float2 Color: COLOR;

float4 LightColor, float4 Lightattenuation, float3 EyeDir)

//Determine the Distance from the light to the vertex and the direction float3 LightDir = LightPos - VertPos; float Dis = length(lightDir); LightDir = LightDir / Dist; //Computer half vector

float3 HalfVect = normalize( LightDir = EyeDir);

//Compute distance based attenuation. this is defined as: //Attenuation = 1/(LA.x + LA.y * Dist + LA * Dist * Dist)

float DistAttn = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.z * Dist * Dist)); float SpecularAttn = pow(saturate ( dot(VertNorm,HalfVect)),32); //Compute finfal lighting

return LightColor * DistAttn * SpecularAttn;

(左图为per-pixel lighting,右图为per-vertex)

上图显示了逐像素和逐顶点光照的差别。当处理高精度多边形模型时,由于每个多边形所覆盖的区域很小,因此插值之后使用逐像素光照的另一个好处是可以在渲染时添加并不存在的表面细节。通过bump map或normal map,可以在像素级再回过头来看看漫反射光线,你需要决定哪些光照元素可以插值之后传送到像素着色器,哪些元素必须逐像素计算。从头这个点积定义了光照的强度,但是对它进行插值的结果通常不正确。因此这一步计算应该移动到像素着色器中,进行逐像表面法线和光矢量是计算点积的要素。通常矢量间的插值是正确的。因此可以在顶点着色器计算他们的值,并传递到像素

每个像素的误差也很小,所以逐顶点光照可以工作的很好。而当处理低模时,这种误差就变的很大了。 别让原本平坦的表面表现出近似的凹凸效果。

到尾,决定漫反射光照的就是表面法线和光线矢量的点积。 素计算。

着色器中,然后进行最终的点积计算。

注意

虽然对法线的插值是正确的,但是插值之后的向量将不再是标准向量。为了对此进行校正,需要在像素着色器中对法线重新进行标准化,可以使用内建的normalize函数。

为了把光照计算移动到像素着色器中,需要先添加两个变量到顶点着色器的输出结构中。可以使用TEXCOORD1和TEX

COORD2语义把这些值传递到像素着色器。代码如下:

struct VS_OUTPUT { }

VS_OUTPUT vs_main(float4 inPos:POSITION,float3 inNormal: NORMAL, {

VS_OUTPUT Out;

Out.Pos = mul( view_proj_matrix, inPos); Out.TexCoord = inTxr;

float4 Pos : POSITION; float2 TexCoord: TEXCOORD0; float3 Normal: TEXCOORD1; float3 LightDir: TEXCOORD2;

接下来修改顶点着色器代码。目前我们只考虑单光源光照的情况,所以先删除原先的光照函数把其中的代码复制到vs_ma你可能已经注意到我们还没有讨论基于距离的衰减值应该在哪里计算。虽然对距离的插值结果不是完全正确,但由于光线衰减值只是一个标量,为了把它传递给像素着色器,而又不浪费一个额外的寄存器来传值,可以把LightDir改为一个flo

in函数中。记住,我们后面回讨论多光源的情况。

的变化有足够容差范围,距离上的微小差别并不会给结果带来明显变化,所以不需要逐像素计算。 at4的矢量,把衰减值作为这个矢量的w分量。修改后的顶点着色器如下:

float2 inTxr: TEXCOORD0)

Out.Normal = inNormal;

}

//Computer and move the light direction to the pixel shader float4 LightDir;

LightDir.xyz = Light1_Position - inPosition; float Dist = length(LightDir.xyz); LightDir.xyz = LightDir.xyz / Dist; //Output the light direction Out.LightDir = lightDir; return Out;

LightDir.w = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.y * Dist*Dist));

在像素着色器中,只需要接收参数,计算点击和颜色就可以了。为了方便,把光照计算单独放到一个名为Light_PointDif

fuse的函数中。本质上来说,这里计算光照的方法和在顶点着色器中是一样的。

float4 Light_PointDiffuse(float4 LightDir, float3 Normal,Float4 LightColor) { }

float4 ps_main(float3 inNormal:TEXCOORD1, { }

很简单,对吧!接下来编写镜面高光像素着色器。整个步骤基本上和我们刚才编写漫反射像素着色器的方法差不多。这一个shader的核心是法线和中间矢量的点积,需要把它们移动到像素着色器中。这意味着需要在顶点着色器中计算光照矢量,中间矢量。先修改顶点着色器的输出结构。

struct VS_OUTPUT { }

在顶点着色器中,需要计算以下值:表面法线,光照矢量,中间矢量和基于距离的衰减值。只需要把先前编写的镜面高光

着色器代码稍作修改就行了。

VS_OUTPUT vs_main(float4 inPos:POSITION,float3 inNormal:NORMAL, float2 inTxr: TEXCOORD0) {

//compute dot product of N and L

float AngleAttn = saturate(dot(Normal,LightDir.xyz)); //compute final lighting

return LightColor * LightDir.w * AngleAttn;

float4 inLightDir : TEXCOORD2) : COLOR

//Compter the lighting contribution for this single light return Light_PointDiffuse(inLightDir,inNormal,Light_Color);

float4 pos: POSITION;

float2 TexCoord: TEXCOORD0; float3 Normal : TEXCOORD1; float4 LightDir: TEXCOORD2; float3 HalfVect: TEXCOORD3;

VS_OUT Out;

Out.Pos = mul(view_proj_matrix, inPos); Out.TexCoord = inTxr; float4 LightDir;

LightDir.xyz = Light1_Position - inPos; flaot Dist = length(LightDir.xyz); LightDir.xyz = LightDir.xyz / Dist;

}

LightDir.w = saturate( 1 / ( LightAttenuation.x + LightAttenuation.y * Dist + LightAttenuation.y * Dist*Dist)); //computer eye vector and the half vector

float3 EyeVector = -normalize(mul(inv_view_matrix,float4(0,0,10,1))+inPos); Out.HalfVect = normalize(LightDir - EyeVector); //Output normal and light direction Out.Normal = inNormal; Out.LightDir = LightDir; return Out;

对像素着色器来说,同样把计算高光的代码作为一个单独的函数:

float4 Light_PointSpecular(float3 Normal,flaot3 HalfVect,float4 LightDir,float4 LightColor) { }

float4 ps_main(float3 inNormal:TEXCOORD1,float4 LightDir:TEXCOORD2, { }

介绍HLSL或者DirectX \\ XNA的书中都很少会详细讲解Semantics,DirectX SDK对此也是草草带过,加上这个单词本身对汉语来说含义比较难懂,因此Semantics常常成了HLSL中鲜为人知的一部分。然而,对于构建复杂Effect系统来说,Semantics作为DirectX \\ XNA和shader之间传输数据的纽带,有相当重要的作用。

float SpecularAttn = pow(saturate(dot(Normal,HalfVect)),32); return LightColor * LightDir.w * SpecularAttn;

float3 HalfVect:TEXCOORD3):COLOR

//simply route teh vertex color to the output

return Light_PointSpecular(inNormal,HalfVect,LightDir,Light1_Color);

Semantics究竟是什么呢?简单来说,它是描述shader中变量的原数据,用来表示某个变量的语义。它与DirectX中D3DDECLUSAGE或者XNA中VertexElementUsage枚举的作用相当类似。

Semantics有什么用呢?Shader程序使用Semantics作为标记,实现数据从CPU到GPU的绑定。

为什么使用Semantics呢? 在Semantics出现之前,shader程序员常常直接使用寄存器名来绑定数据。这样做有两个缺点,首先,编译器无法对代码进行优化,其次,维护移植起来也相当不方便。Semantics隐藏了寄存器的复杂性,所有积存器的分配都由编译器来为你处理,同时,对外提供了一个统一的接口。

如何使用Semantics呢?先来看Semantics的语法:

[ Modifiers] ParameterType ParameterName [ : Semantic] = [ Initializers] 比如:

float4x4 WorldMatrix : WORLD;

冒号前面的部分,声明了一个变量myWorldMatrix,你也许希望用它来表示世界坐标变换。但是,编译器对此一无所知,

只会把它当作一个普通的float4x4变量来对待。然而,添加了冒号以及标准Semantics WORLD之后,无论变量叫什么名字,编译器都会知道这个变量将被当作坐标变换矩阵来使用。注意,Semantics本身并不区分大小写,只是习惯上用大写表示。

从类型上来看,Semantics可以分为vertex\\pixel semantics和普通变量semantics两种。

对于标记了vertex\\pixel semantics值的变量来说,顶点会自动把vertex shader输入顶点或输出数据中相应的值绑定到变量

上。

本文来源:https://www.bwwdw.com/article/2cfg.html

Top