HLSL基础语法
什么是shader lanuage?
shader lanuage称为着色语言,是一种图形编程语言,适用于编程着色器效果(描述表面、体积和对象),它基于物体本身属性和光照条件,计算每个像素的颜色值
shader lanuage被定位为高级语言,有三个,分别是opengl的"GLSL",direct3d的"HLSL",NVIDIA的"cg",都是类c语言
GLSL的优点在于它的跨平台性,它可以在多种平台上工作,但这种跨平台性是由于OpenGL没有提供着色器编译器,而是由显卡驱动来完成着色器的编译工作。也就是说,只要显卡驱动支持对GLSL的编译它就可以运行。这种做法的好处在于,由于供应商完全了解自己的硬件构造,他们知道怎样做可以发挥出最大的作用
而对于HLSL,是由微软控制着色器的编译,就算使用了不同的硬件,同一个着色器的编译结果也是一样的(前提是版本相同)。但也因此支持 HLSL的平台相对比较有限,几乎完全是微软自已的产品,如Windows、 Xbox 360、PS3等。这是因为在其他平台上没有可以编译HLSL的编译器
Cg则是真正意义上的跨平台。它会根据平台的不同,编译成相应的中间语言。Cg语言的跨平台性很大原因取决于与微软的合作,这也导致CG语言的语法和HLSL非常相像,Cg语言可以无缝移植成HLSL代码。但缺点 是可能无法完全发挥出OpenGL的最新特性
为什么需要shader lanuage?
在可编程管线出现前,为编写着色器代码,开发人员需要学习汇编语言,不方便也复杂,为了提高开发效率,shader lanuage油然而生,着色语言编写的代码会被翻译为与机器无关的汇编语言,随后再被交于显卡驱动翻译成GPU可以理解的语言——机器语言
draw call
什么是draw call
Draw Call是CPU调用的图像编程接口,来命令GPU进行渲染。如DirectX中的DrawIndexedPrimitive()或DrawInstanced()
为什么Draw Call调用太多会影响帧率
有一个常见的误区,Draw Call中造成性能问题的元凶是GPU,认为GPU上的状态切换是耗时的,但其实并不是,真的元凶是CPU
每次调用draw call前,CPU需要向GPU发送许多内容,包括数据、状态和命令等,在这一阶段CPU需要完成许多工作,但GPU的渲染速度十分快,因此渲染速度往往快于CPU提交命令的速度,若draw call调用次数过多,CPU会将大量时间花费在提交draw call上,造成GPU过剩
如何减少draw call
提交大量很小的draw call会造成GPU过剩,那么很显然我们可以把一系列小的draw call合并成大的draw call
不过这种方式需要在CPU的内存中合并网格且合并耗时,因此此方法更适于静态物体
值得注意的是
- 避免使用大量很小的网格。当不可避免地需要使用很小的网格结构时,考虑是否可以合并它们
- 避免使用过多的材质。尽量在不同的网格之间共用同一个材质
着色器介绍
- 什么是着色器?
着色器又称处理器,是GPU管线上一些可高度编程的阶段,如Programmable Vertex Processor意为可编程顶点处理器,又称顶点着色器.而着色器是一个硬件单元,可以运行顶点程序。依靠着色器,我们便可以控制流水线中的渲染细节着色器的运行过程如下
-
重要的着色器
-
顶点着色器
它是一个可编程着色器,处于GPU管线的第一阶段。主要任务是从GPU寄存器中提取输入来自CPU的一系列顶点和索引,来完成顶点的坐标空间变化、法向量空间转换、光照计算等,最后将输出的数据传送至指定的寄存器中
-
片元着色器
它是一个可编程着色器,又称为像素着色器。它的输入是上个阶段顶点信息插值后的输出结果,输出是一个或多个颜色值。主要任务是通常获取"纹理坐标、光照信息",并根据这些信息及应用程序传递的纹理信息进行
每个片段的颜色计算
,最后送至光栅化模板注:片元这个形容更恰当,因为片元和像素其实并不相同,像素是经过一一计算后最终输出在屏幕上的;而片元任一三维顶点在光栅化后的数据集合,这些顶点还未经过深度值测试
-
输入数据流
输入数据流有两类
- Varying inputs:即数据流输入图元信息的各种组成要素。从应用程序输入 到 GPU的数据除了顶点位置数据,还有顶点的法向量数据,纹理坐标数据等
- Uniform inputs:该数据在整个着色器执行过程中是恒定的,表示一些与三维渲染有关的离散信息数据,这些数据通常由应用程序传入,如材质对光的 反射信息、运动矩阵等
变量
HLSL变量类似于c++中的变量,具有命名限制、取决于声明它们的位置的范围属性、标准数据类型,不同的是,定义其他数据类型用以提高使用3d数据的性能
声明模板
[Storage_Class] [Type_Modifier] Type Name [Index] [: Semantic] [: Packoffset] [:Register];
-
Storage_Class(存储类型)
-
用于为编译器提供关于变量范围和生存期的提示
-
可以按任意顺序指定修饰符
-
value description extern 与c++一样,将全局变量标记为着色器的外部输入 nointerpolation 将顶点着色器的输出传递给像素着色器之前,不要对它们进行插值 shared 标记一个用于在效果之间共享的变量; 这是对编译器的提示 groupshared 为计算着色器的线程组共享内存标记一个变量 static 与c++一样,标记一个局部变量,使其初始化一次。若声明不对其进行显式初始化,将默认初始化为0 volatile 与c++一样,标记经常更改的变量;仅适用于局部变量 uniform 标记一个变量,该变量在整个着色器执行过程中是恒定的。此值可在c++应用层改变,但在着色器执行时,其值不变
uniform变量在着色器程序外进行初始化
-
-
Type_Modifier(类型修饰符)
- 若未指定类型修饰符,则编译器使用 column _ main 作为
默认值
value description const 与c++一样,标记一个不能被着色器更改的变量,必须在变量声明中初始化 row_major 标记一个变量,该变量在一行中存储4个组件 column_major 标记一个变量,该变量在一列中存储4个组件,以优化矩阵运算 - 若未指定类型修饰符,则编译器使用 column _ main 作为
-
Type
HLSL支持的内部数据类型
-
标量类型
类型名 含义 bool ture or false int 32位有符号整数 uint 32位无符号整数 dword 32位无符号整数 half 半数据类型,16位浮点值。仅用于语言兼容性
半数据类型不能用于统一的全局变量(若有需要,请使用/Gec 标志)float 32位浮点值 double 64位浮点值。不能用于流的输入和输出,请使用一对uint数据类型,再使用 asuint 函数将每个 double 打包到这对 uint 中,并使用 asdouble()将这对 uint 解压至 double 中 min16float 最小16位浮点值 min10float 最小10位浮点值 min16int 最小16位有符号整数 min12int 最小12位有符号整数 min16uint 最小16位无符号整数 string ASCII 字符串。没有接受字符串的操作或状态,但是可以查询字符串参数和注释 -
向量类型
-
向量类型由两部分组成,一个向量的每个分量必须具有相同的类型
-
标量类型
-
分量的数量,在1-4间
如:
向量类型 含义 float2 2d向量,每个分量都是float类型 float3 3d向量 float4 4d向量 -
-
初始化向量
-
以类似数组或构造函数的语法来初始化向量
float3 v = {1.0f, 2.0f, 3.0f}; float2 w = float(x,y); float4 u = float4(w, 3.0f, 4,0f)
-
-
访问分量
-
以数组下表语法来访问
vec[i] = 1.0f;
-
以规定的分量名x、y、z、w、r、g、b、a像访问结构体成员一样来访问分量
vec.x = vec.r = 1.0f;
-
-
重组
(swizzleing)将向量u的分量复制到向量v的分量,可以乱序方式进行
float4 u = {1.0f, 2.0f, 3.0f, 4.0f}; float4 v = {0.0f, 0.0f, 5.0f, 6.0f}; v = u.wyyx; //v = {4.0f, 2.0f, 2.0f, 1.0f}
-
-
矩阵类型
-
包含1-16个分量,每个分量类型必须相同
-
矩阵类型由三部分组成
- 标量类型
- 行数,1-4的正整数
- 列数,1-4的正整数
如:
类型名称 含义 float2x2 2x2矩阵,每个元素类型都为float float3x3 3x3矩阵 float4x4 4x4矩阵 float3x4 3x4矩阵
-
-
访问
-
以二维数组的双下标进行访问
M[i][j] = value;
-
以访问结构体成员的方式进行访问
//以1作为起始值的索引 M._11 = M._12 = M._13 = M._14 = 0.0f; M._41 = M._42 = M._43 = M._44 = 0.0f; //以0作为基准值的索引 M._m00 = M._m01 = M._m02 = M._m03 = 0.0f; M._m30 = M._m31 = M._m32 = M._m33 = 0.0f;
-
引用矩阵中特定的行向量。以数组的单下标实现
float3 row = M[i];
-
-
-
struct
与c++的struct定义方式基本相同,区别在于HLSL的struct不含有成员函数
-
typedef
与c++的typedef功能完全相同
typedef float3 point;
-
array(数组)
与c++相同
half p[3]; float M[4][4];
-
-
Index(索引)
hlsl支持数组,语法和含义与c++类似
-
Semantic(语义)
-
什么是语义、语义词和语义绑定?
语义:两个处理阶段间的输入/输出数据和寄存器间的桥梁,也表示数据的预期用途
语义词:表示
输入图元的含义
(位置信息,还是法向量信息)和图元数据存放的硬件资源
(寄存器,缓冲区) 语义绑定:将着色程序的输入输出变量和语义词绑定
-
输入语义和输出语义
-
为什么需要输入语义和输出语义?
类似c++这样的语言,数据从一端流向另一端,是因为提供了数据存放的内存位置。但着色器语言并不支持指针和引用,且图形硬件处理过程中,数据通常暂存在寄存器中。因此着色器语言便进入了语义绑定,指定数据存放的位置就是将输入/输出数据和寄存器做映射关系
根据输入语义,GPU从指定寄存器取数据;根据输出语义,放到指定的寄存器
-
-
语义绑定的四种方式
- 绑定语义放在函数的参数列表的参数声明后面
- 绑定语义可以放在结构体(struct)的成员变量后面
- 绑定语义词可以放在函数声明的后面。表示函数需要反馈符合语义的值
-
System-Value Semantic(系统值语义)
系统值语义和普通的语义并无不同,只是被用于表示特殊的含义,所有系统值都以 SV_前缀开头,如SV _ POSITION
-
语义含义
顶点着色器语义
input description type COLOR[n] 镜面反射颜色 float4 NORMAL[n] 法线向量 float4 POSITION[n] 空间中的顶点位置 float4 TEXCOORD[n] 纹理坐标 float4 PSIZE[n] 点大小 float Output Description Type COLOR[n] 漫反射或镜面反射颜色 float4 POSITION[n] 齐次空间中顶点的位置 float4 PSIZE 点大小 float 像素着色器语义
Input Description Type COLOR[n] 漫反射或镜面反射颜色 float4 TEXCOORD[n] 纹理坐标 float4 VPOS 屏幕空间中的像素位置(x,y) float2 Output Description Type COLOR[n] 输出颜色 float4 DEPTH[n] 输出深度 float 系统值语义
语义名称 说明 类型 SV_Position 在顶点着色器中作为输出变量和函数的语义,用于指出该输出元素存有齐次裁剪空间中的顶点信息,且该值不能改变
在像素着色器中作为输入变量的语义,用于指出该变量存储像素在屏幕空间的坐标,像素着色器中的 SV _ POSITION 是旧时 VPOS 语义的一种替代float4 SV_Target 用于像素着色器作为输出值的绑定语义,与COLOR并无差异。在DX10之前以COLOR表示,DX10之后便用SV_Target替换COLOR
但在某些开发环境下,不可以用COLOR来替换SV_Targetfloat4 -
注意事项
- 一般来说,流水线阶段之间传递的数据是完全通用的,系统不进行唯一的解释
- 在着色器阶段之间传递的所有变量都需要语义
- 语义只对两个处理阶段的输入/输出数据有效,在内部函数中无效
-
-
Packoffset
可选参数,用于指定变量存储在常量缓冲区中的位置
例:
//在hlsl中,默认按float4对齐数据 cbuffer MyBuffer { float4 Element1 : packoffset(c0); //在MyBuffer常量缓冲区的c0位置存储Element1 float1 Element2 : packoffset(c1); //在MyBuffer常量缓冲区的c1位置存储Element2 float1 Element3 : packoffset(c1.y); //在MyBuffer常量缓冲区的c1.y位置存储Element3 }
-
Register
可选参数,用于手动将着色器变量/资源分配给特定寄存器
例:
Texture2D gDiffuseMap : register(t0); //将2D纹理资源绑定到纹理寄存器槽0
强制类型转换
HLSL中的强制类型转换与c相同
float f = 5.0f;
float4x4 m = (float4x4)f; //将标量复制给m中每个元素
float3 n = float3(...);
float3 v = 2.0f * n - 1.0f; //等同于 float3 v = 2.0f * n - float3(1.0f, 1.0f, 1.0f);
常用关键字
asm, bool, break, case, class, compile, const, continue, default, do, double, dword,
else, extern, false, true, float, for, half, if, in, inline, inout, int, matrix, namespace, NULL, out, pass, return, register, sampler, shared, static, string, struct, switch, typedef, uint, uniform, unsigned, vector, vertexshader, void, volatile, while,
texture, technique, pixelshader, discard
运算符
HLSL的运算符规则与c++基本相同,除了以下几种
取模运算符%
可用于整数和浮点数,且操作数的正负号相同- HLSL的部分运算以分量为基准
- 对于二元运算
- 若操作符左右操作数
维度
不同,维度较小的变量类型被转换为维度较大的变量类型 - 若操作符左右操作数
类型
不同,低精度变量被转换为高精度变量
- 若操作符左右操作数
如:
//乘法
float4x4 A;
float4x4 B;
A*B; //分量式乘法
mul(A,B); //矩阵乘法
//比较运算符
float4 u = {1.0f, 0.0f, -3.0f, 1.0f};
float4 v = {-4.0f, 0.0f, 1.0f, 1.0f};
float4 b = (u == v); // b = (false, true, false, true)
控制流
HLSL支持的控制流与c++相似.以下语句语法与c++完全相同
- return
- if & if...else
- while
- for
- do...while
- break
- continue
- switch
函数
-
HLSL中函数的属性
-
函数采用类c++语法
-
参数只能按值传递
-
不支持递归
-
只有内联函数
-
与c++相较下,多了in、out、inout关键字
-
in:目标函数执行前,此修饰符所指定的参数应从调用此函数的程序中复制有输入的数据.所有参数默认为in
-
out:目标函数返回时,此修饰符所指定的参数应当已经复制该函数中的最后计算结果
-
因为HLSL没有引用和指针,需要用out返回数值
-
若一个参数被out修饰,此参数只能用于输出数据,不可用于输入数据,也就是说不能被复制
void func(float x, out float y) { y = x * x; }
-
inout:表示参数兼有in和out属性
-
-
-
-
内置函数
-
abs(x):return |x|
-
acos(x):return x 的反余弦值
-
atan2(y, x):return 坐标点(0,0)与坐标点(y,x)的夹角大小(弧度),范围[-Π, Π]
-
ceil(x):return 大于或等于x的最小整数值
-
clamp(x, min, max):将x限制在[min, max]内
-
clip(x):若x小于零,则丢弃当前像素
-
cos(x):return x的余弦值,x单位为弧度
-
cross(u, v):return u和v的叉积
-
ddx(u):return u 相对于 screen-space x坐标的偏导数
-
ddy(u):return u 相对于 screen-space y坐标的偏导数
-
degrees(x):将x从弧度转换为角度
-
distance(x, y):return 向量 x 和 y的距离
-
dot(x, y):return 向量 x 和 y的点积
-
exp(x):return 以 e 为底,指数为x的值
-
exp2(x):return 以2为底,指数为x的值
-
faceforward(n, i, ng):将表面法线ng翻转为与向量i方向相反的向量,n为翻转后的值并输出它
-
floor(x):return 小于或等于x的最大整
-
fmod(x, y):return x/y 的浮点余数
-
frac(x):return x 的小数部分
-
length(x):return 向量x的长度
-
lerp(a, b, t):对a和b进行线性插值
-
max(x, y):return x 和 y中的最大值
-
min(x, y):return x 和 y中的最小值
-
mul(x, y):对x 和 y进行矩阵相乘。x若为向量则为行向量,y若为向量则为列向量
-
noise(x):使用 Perlin-noise 算法生成一个随机值
-
normalize(x):规范化向量x
-
pow(x, y):return x的y次方
-
radians(x):将x从角度转为弧度
-
reflect(v, n):根据表面法线n与入射向量v,计算反射向量
-
refract(v, n, eta):根据表面法线n与入射向量v,两种材质的折射率之比eta计算折射向量
-
round(x):将x四舍五入为最接近的整数
-
rsqrt(x):return x的平方根的倒数
-
saturate(x):将x限制在[0,1]内
-
sign(x):return x的符号
-
sin(x):return x 的正弦值
-
smoothstep(min, max, x):如果 x 在[ min,max ]范围内,对x进行[0,1]内的平滑插值
-
sqrt(x):return x的平方根
-
step(y , x):若 x 大于或等于 y ,则为1; 否则为0
-
tan(x):return x的正切值
-
-
常量缓冲区的封装规则
HLSL中,常量缓冲区会以padding(补齐填充)的方式,将其中的元素都包装在4D向量。但
一个单独的元素不能被分开并横跨两个4D向量
cbuffer cb : register(b0) { float3 Pos; float3 Dir; } //封装如下 vector 1 : (Pos.x, Pos.y, Pos.z, empty) vector 2 : (Dir.x, Dir.y, Dir.z, empty)
reference
High-level shader language (HLSL) - Win32 apps | Microsoft Learn
Directx12 3D 游戏开发实战
GPU编程与CG语言之阳春白雪下里巴人