第四章 Hello,Shaders
本章,会编写第一个shaders。介绍HLSL语法,FX文件格式,数据结构等等。学完本章,你就具备了深入学习图形编程的基础知识。
Your First Shader
使用一种新的编程语言编写第一个程序时都会使用经典的编程例子“Hello,World!”,程序输出就是一行文字“Hello,World!”。我们遵守这一历史悠久的传统,编写第一个shader程序“Hello,Shaders!”,但是这次的输出是一种固定的颜色渲染到一个object上。
首先,启动NVIDIA FX Composer并创建一个新的工程。打开Assets panel,在Materials图标上点击鼠标右键,并选择Add Material from New Effect菜单项。然后在Add Effect对话框中选择HLSL FX,并点击Next进入下一步。
图4.1 NVIDIA FX Composer Add Effect dialog box
在一个对话框中,选择空模板,并命名为HelloShader.fx(如图4.2)。
图4.2 NVIDIA FX Composer Select HLSL FX Template dialog box.
在Effect Wizard(Effect向导)最后的对话框中点击Finish就完成了Effect的添加。如果每一步都完成了,你应该能在Editor panel中看到HelloShaders.fx文件,并在Assets panel中有对应的HelloShaders和HelloShaders_Material objects列表。需要注意的空effect模板创建的shader文件并不是空文件,因为NVIDIA FX Composer会自动添加一部分代码。实际上这段代码正是编写第一个shader所需要的,但由于是用于DirectX
9,因些删除它并用列表4.1中的内容代替。然后一步一步的讲解这段代码。
列表4.1 HelloShaders.fx
cbuffer CBufferPerObject { float4x4 WorldViewProjection : WORLDVIEWPROJECTION; } RasterizerState DisableCulling { CullMode = NONE; }; float4 vertex_shader(float3 objectPosition : POSITION) : SV_Position { return mul(float4(objectPosition, 1), WorldViewProjection); } float4 pixel_shader() : SV_Target { return float4(1, 0, 0, 1); } technique10 main10 { pass p0 { SetVertexShader(CompileShader(vs_4_0, vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_4_0, pixel_shader())); SetRasterizerState(DisableCulling); } }
Effect Files
通过单独编译的shaders,Direct3D管线阶段是可编程的。比如,可以在一个文件中存放vertex shader(通常文件后缀名为 .hlsl),在另一个不同的文件中存放pixel shader。这种配置下,每一个文件必须包含一个完整的shader。相比之下,HLSL Effect文件支持把多个shaders,函数,和渲染状态合并到单个文件中。这种文件格式正是本书中使用的格式,表4.1中已经使用了。
Constant Buffers
在HelloShaders.fx文件的头部,有一个由cbuffer开始的代码快。这表示一个常量buffer,用于管理一个或多个shader常量。一个shader常量输入到CPU中再发送给着色器,对单个绘制调用操作的所有primitives都保持不变。另一方面,cbuffers存储变量,并且是常量。从GPU的角度看,在单个绘制操作primitives过程中,cbuffers是常量,但从CPU的角度来看,从一个绘制调用到下一个阶段是可变的。
在HelloShaders.fx文件中,只有一个cbuffer包含一个shader常量,float4×4类型的WorldViewProjection。这是一个C风格的变量,声明类型为单精度浮点型的4×4 matrix。WorldViewProjection变量表示由World-View-Projection串联的矩阵,并用于每一个object。回想一下第二章,“A 3D/Math Primer”,该矩阵只需要单个变换就可以把vertices从object space变换到world space再到view space,homogeneous
space。把World,View和Projection三个矩阵分别传递到effect中,然后执行三次不同的变换,也可以产生同样的结果。除非你有特殊的理由要这么做,否则输入更少的数据,执行更少的shader指令是更好的选择。
注意变量声明冒号后的WORLDVIEWPROJECTION文字,这是一个语义,提示这是在CPU上运行的应用程序想要使用的变量。这种语义使得应用程序开发人员可以不需要提前知道shader常量的名称。要这个示例中,可以把float4×4类型的变量任意命名为WVP或WorldViewProjection,而不用担心对CPU程序的影响,因为CPU通过WORLDVIEWPROJEC语义访问该变量而不是变量名。Shader中有各种各样的通用语义,对于shader常量来说所有语义都是可选的。然而,在NVIDIA FX Composer,WORLDVIEWPROJECTION语义是必需的;需要与shader常量关联以用于接收每帧的WVP组合矩阵的更新。
What’s in a Name?
在HelloShaders effect中,constant buffer被命名为CBufferPerObject。这种命名本身没有什么特别的,但是表明了cbuffer中shader常量的更新频率。PerObject buffer表示CPU要更新effect中每个object对应buffer的数据。
作为对比,CBufferPerFrame则是指该buffer的数据每一帧更新一次,允许多个objects使用相同的shader常量进行渲染。
以这种方式管理cbuffers可以实现更高效的更新。当CPU改变了cbuffer中的任何shader常量,都需要更新整个cbuffer。因此,最好的方法是根据shader常量的更新频率进行分组。
Render States
Shaders无法定义Direct3D管线中非可编程阶段的行为,但是可以通过渲染状态的objects实现自定义。比如,通过一个RasterizerState object可以自定义渲染状态。在Shader中可以设置多种渲染状态,但是要在后面的章节中讨论。现在只需要知道RasterizerState object DisableCulling(如列表4.2所示)。
列表 4.2 RasterizerState declaration from HelloShaders.fx
RasterizerState DisableCulling { CullMode = NONE; };
在第三章,“Tools of the Trade”,简要讲述了vertex绕序和背面消除。DirectX默认情况下,把逆时针显示的vertices当做背面,而且不会绘制。但是在NVIDIA FX Composer中默认模型(Sphere,Teapot,Torus,Plane)具有相反的绕序方向。如果不修改或者禁用culling mode,Direct3D会消除我们认为属于正面的三角形。因为,在NVIDIA FX Composer中,只需要禁用culling,通过指定CullMode = NONE。
注意:
之所以在NVIDIA FX Composer中存在culling问题,是因为FX Composer同时支持DirectX和OpenGL渲染API。这两种渲染库默认的正面绕序不一样,而NVIDIA FX Composer默认使用OpenGL的方式。
The Vertex Shader
下一个要分析的HelloShaders代码是vertes shader,如列表4.3所示。
列表4.3 The vertex shader from HelloShaders.fx
float4 vertex_shader(float3 objectPosition : POSITION) : SV_Position { return mul(float4(objectPosition, 1), WorldViewProjection); }
该段代码类似一个C风格的函数,但是有一些关键性的差异。首先,vertex shader要完成的工作不同。每一个vertex都以object space进入shader,然后WorldViewProjection矩阵把vertex变换成homogeneous clip space。正常情况下,就是一个vertes shader所需要完成的最少的操作。
Vertex shader的输入是HLSL中float3的数据类型,该种类型用于存储3个单精度浮点型数据,命名为objectPositon表示就是一个坐标空间。与objectPositon参数对应的语义是POSTION。表示该变量包括一个vertex postion。这与shader常量中使用的语义在概念上是一样的,用来指明参数的用处。然而,这些语义也用于在shader阶段连接shader的输入和输出(比如,连接input-assembler和vertex shader阶段),因此变量需要对应的语义。至少,vertex
shader必须接受一个具有POSTION语义的变量并返回SV_Potion语义的变量。
注意:
带有SV_前缀的语义是system-value语义,最早由Direct3D 10中提出的。这些语义为管线指定了一种特殊的含义。例如,SV_Position表明对应的输出将会包含一个用于rasterizer阶段的已经过变换的vertex position。而对于non-system-value的语义,包括一系列标准的语义,都是通用的语义无法在管线的外部解释。
在vertex shader代码中,调用了HLSL内置函数mul。该函数用于对两个参数进行矩阵相乘操作。如果第一个参数是一个vector,会被看一个行向量(第二个参数就是一个行优先矩阵)。相反,如果第一个参数是一个矩阵,就会当作一个列优先矩阵,第二个参数就是一个列向量。由于大部分变换都使用行优先矩阵,因此这样使用mul函数mul(vector, matrix)。
需要注意的是,mul函数的第一个参数,是使用objectPosition(float3类型)和数字1构造成的一个float4类型参数。这一步是必须的,因为向量的列数必须和矩阵的行数相匹配。因为要做变换操作的向量是一个position,只需要把第4个float值(w分量)指定为1。要是向量表示的是一个direction,w分量应该被设置为0。
The Pixel Shader
与vertes shader一样,HelloShaders中的pixel shader也只有一行代码(如列表4.4)。
列表4.4 The pixel shader from HelloShaders.fx
float4 pixel_shader() : SV_Target { return float4(1, 0, 0, 1); }
Pixel shader的返回值是float4类型,并指定为SV_Target语义。表明输出将被存储到render target绑定要output-merger阶段。通常情况下,render target是一个映射到屏幕上的texture,称为back buffer。这个名称来自于双缓冲的技术,这种技术使用两个buffers来减少两个(或以上)帧同时显示时产生的画面抖动,以及其他的残影。相反,在视频设备显示一个front buffer时,所有的输出都渲染到一个back buffer中。当一帧渲染完成后,就交换这两个buffer,显示器上就会显示最新渲染的帧。交换操作通常与显示器的刷新周期一致,用于避免残影。
Pixel shader的输入是一个32位的颜色值,Red,Green,Bluen,和Alpha(RGBA)四个通道各占8-bit。所有的颜色值都是浮点型,[0.0, 1.0]对的整形范围是[0, 255]。在这个例子,Red通道被设置为1,表示每一个pixel被渲染成红色。由于没有使用color blending,所以Alpha通道的值没有影响。如果使用了color blending,Alpha值为1表示一个完全不透明的pixel。在第8章,“Gleaming the Cube”将详细讲解color blending。
注意:
HelloShaders中的pixel shader并没有显示的输入参数,但是不要感到困惑。传递到pixel shader中的homogenous clip space position(齐次裁剪空间坐标点)来自于rasterizer阶段。但是,这个过程是在后台执行的,没有显示声明为pixel shader的输入。
在下一章中,将会讲解如何在pixel shader输入额外的参数。
Techniques
HelloShaders effect的最后部分是使用technique把verter shader和pixel shader整合到一起(见列表4.5)。
列表4.5 The technique from HelloShaders.fx
technique10 main10 { pass p0 { SetVertexShader(CompileShader(vs_4_0, vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_4_0, pixel_shader())); SetRasterizerState(DisableCulling); } }
一种technique通过一组effect passes来实现一种特定的渲染序列。每一个pass都设置渲染状态,并把shaders与对应的管线阶段关联。在HelloShaders示例中,只使用了一种technique(命名为main10)并且只有一种pass(命名为p0)。但是,effects可以包含任务数量的techniques,而且每种techniques可以包含任意数量的passes。目前,所有的techniques都只包含一种pass。在第四部分,“Intermediate-Level Rendering
Topics”,将会讨论包含多个passes的techniques。
注意例子中使用的关键字technique10,表示这是一种Direct3D 10的technique,对于DirectX 9的techniques,但DirectX 9的techniques没有版本后缀。Direct3D 11 techniques的关键字为technique11。不幸的是,NVIDIA FX Composer的当前版本不支持Direct3D 11。但是在学习shader开发的开始阶段,不需要使用Direct3D 11中的特定功能,因此这不是问题。在第三部分,“Rendering with DirectX”,将会使用Direct3D
11 techniques。
另外需要注意的是SetVertexShader和SetPixelShader声明中的vs_4_0和ps_4_0参数。这些参数值指定了调用CompileShader函数编译shaders时第二个参数用到的shader配置。Shader配置好比如shader模型,定义了用于支持对应shaders的图形系统的能力。编写本书的时候,已经有5个主要的shader版本(以及一些次要的版本);最新的shader模型是版本5.每一个shader模型都以各种不同的方式扩展了以前版本的功能。但通常情况下,shaders的功能都随着新的shader模型而增加。Direct3D
10引入了shader model 4,用于所有Direct3D 10 techniques中。而Direct3D 11引入了shader model 5,并用于所有Direct3D 11 techniques中。
Hello, Shaders! Output
现在可以开始显示HelloShaders effect的输出了,首先需要编译effect,可以点击Build,Rebuild All或者Build,Compile HelloShaders.fx菜单。也可以使用快捷键F6(Rebuild All)或Ctrl+F7(Compile Selected Effect)。每次修改代码都要重新编译。
下一步是,指定使用Direct3D 10 渲染API,可以在工具栏的下拉菜单中选择(位于工具栏最右侧,默认情况下很可能是Direct3D 9)。现在可以打开NVIDIA FX Composer中的Render panel,该panel默认位于右下角。选择主菜单上的Create-->Sphere或者点击工具栏上的Sphere图标就可以在Render panel中创建一个sphere。最后,从Materials panel或Assets panel中把HelloShaders_Material拖放到Render
panel中的Sphere object上。就能看到类似于图4.3中的画面。
图4.3 HellShaders.fx applied to a sphere in the NVIDIA FX Composer Render panel.
花了这么多精力才做这么点效果,可能会让你觉得很无趣,但是实际上已经完成了很多。花点时候多尝试下shader的输出,改变一下pixel shader的RGB通道值,看看会发生什么。