第四章 Hello,Shaders

第四章 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进入下一步。

第四章 Hello,Shaders_第1张图片
图4.1 NVIDIA FX Composer Add Effect dialog box

在一个对话框中,选择空模板,并命名为HelloShader.fx(如图4.2)。

第四章 Hello,Shaders_第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中的画面。

第四章 Hello,Shaders_第3张图片
图4.3 HellShaders.fx applied to a sphere in the NVIDIA FX Composer Render panel.
花了这么多精力才做这么点效果,可能会让你觉得很无趣,但是实际上已经完成了很多。花点时候多尝试下shader的输出,改变一下pixel shader的RGB通道值,看看会发生什么。



你可能感兴趣的:(Rendering,with,DirectX,&&,HLSL)