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

图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通道值,看看会发生什么。



时间: 2024-10-08 17:20:33

第四章 Hello,Shaders的相关文章

第四章

第四章 源代码的下载和编译 1.下载.编译和测试Android源代码 配置Android源代码的下载环境 ①创建一个存放下载脚本文件(repo)的目录 # mkdir  ~/bin # PATH=~/bin:$PATH ②下载repo脚本文件 # curi http://dl-ssl.google.com/dl/googlesource/git-repo/repo > -/bin/repo # chmod a+x ~/bin/repo ③创建存放Android源代码的目录 # mkdir and

《UML精粹》 第四章 时序图

第四章 时序图 一般来说,我们会在一张时序图中画出某个情节的相关行为,图种会秀出这个使用案例(use case)里面可能出现的一些对象,以及在对象间传送的信息. 本章将通过一个简单情节,做时序图各方面的相关讨论.假设我们现在有一份订单,并且准备调用它的一个命令,算出这份订单的价格.为了达到这个目的,订单需要产看它里面所拥有的一些订单明细.决定它们的价格,价格决定方式是以订单明细中所包含产品之定价规则为基础决定的.对所有订单明细做完上述动作之后,接下来订单要算出整个折扣,这时候它是以跟客户绑在一起

Java 线程第三版 第四章 Thread Notification 读书笔记

一.等待与通知 public final void wait() throws InterruptedException 等待条件的发生. public final void wait(long timeout) throws InterruptedException 等待条件的发生.如果通知没有在timeout指定的时间内发生,它还是会返回. public final void wait(long timeout, int nanos) throws InterruptedException

2017.06.29数据挖掘基础概念第四章

第四章39.为什么在进行联机分析处理(OLAP)时,我们需要一个独立的数据仓库,而不是直接在日常操作的数据库上进行 1.提高两个系统的性能 2.操作数据库支持多事务的并发处理,需要并发控制和恢复机制,确保一致性和事务的鲁棒性 3.两者有着不同的数据的结构.内容和用法40.什么是数据仓库 数据仓库是一种数据库,它与单位的操作数据库分别维护,数据仓库系统允许将各种应用系统集成在一起,为统一的历史数据分析提供坚实的平台,对信息处理提供支持,是一个面向主题的.集成的.时变得.非易失的数据集合,支持管理者

构建之法学习(第四章 两人合作)

第四章 两人合作 1.代码规范  1)代码风格规范.主要是文字上的规定,看似表面文章,实际上非常重要. *原则:简明,易读,无二义性 *缩进:4个空格 *行宽:行宽必须限制,可以限定为100字符 *括号:在复杂的条件表达式中,用括号清除地表示逻辑优先级 *断行与空白的{}行:推荐格式如下 if ( condition ) {        DoSomething(); } else {       DoSomethingElse(); } *分行:不要把多条语句放在一行上.并且,不要把多个变量定

第四章—变量,作用域和内存问题(二)

第四章-变量,作用域和内存问题(二) JS没有块级作用域 js没有块级作用域,这个概念容易导致误解,这里就区分下几个情况,大家好好参考下: 我们知道,在其他类C的语言中,由花挂号封闭的代码块都有自己的作用域.但是在JS中,却没有块级作用域: 这里if(true){}代表条件永真,永远执行这条.if(false){}的话就是永远不执行这条. 这个代码执行之后,在if语句定义的变量,在if语句外可以访问的到.在if语句中的变量声明会将变量添加到当前的执行环境中(这里是全局环境). 还有如下的两个例子

Shell脚本学习指南 [ 第三、四章 ] 查找与替换、文本处理工具

摘要:第三章讨论的是编写Shell脚本时经常用到的两个基本操作.第四章总共介绍了约30种处理文本文件的好用工具. 第三章 查找与替换 概括:本章讨论的是编写Shell脚本时经常用到的两个基本操作:文本查找.文本替换. 3.1  查找文本 如需从输入的数据文件中取出特定的文本行,主要的工具为grep程序.POSIX采用三种不同grep变体:grep.egrep.fgrep整合为单个版本,通过不同的选项,分别提供这三种行为模式.who | grep -F root上面使用-F选项,以查找固定字符串r

javascript高级程序设计 第十四章--表单脚本

javascript高级程序设计 第十四章--表单脚本 在HTML中表单由<form>元素表示,在js中表单对应的是HTMLFormElement类型,这个类型也有很多属性和方法:取得表单元素的引用还是为它添加id特性,用DOM操作来获取表单元素:提交表单:把<input>或<button>元素的type特性设置为"submit",图像按钮把<input>元素的type特性设置为"image",也可以调用submit(

Java语言程序设计(基础篇) 第四章 数学函数、字符和字符串

第四章 数学函数.字符和字符串 4.2 常用数学函数 方法分三类:三角函数方法(trigonometric method).指数函数方法(exponent method)和服务方法(service method) 4.4 String类型 String类型不是基本类型,而是引用类型(reference type).