DX学习笔记(五):绘制几何体2
帧资源
上一篇在绘制每一帧都会进行一次CPU与GPU的同步,目的是
- GPU未结束命令分配器中所有命令执行之前,不能重置,如果重置,那么GPU当前还未处理的命令就会被清除
- 对于常量缓冲区的数据而言,不同步的话会导致数据异常
所以我们用FlushCommandQueue
来保持同步
但是有2个缺点导致性能浪费
- 每一帧起始阶段GPU无任务,处于闲置状态
- 帧结尾CPU处于闲置状态等待GPU完成绘制
帧资源
CPU会比GPU提前两帧工作,以保持GPU可持续工作
1 | //存有CPU为构建美珍命令列表所需的资源 |
1 |
|
然后我们的应用程序类将用一个vector容器实例化3个帧资源对象
1 | const int gNumFrameResources = 3; |
然后在CPU处理第n帧的算法是如下
1 | void ShapesApp::Update(const GameTimer& gt) |
1 | void ShapesApp::Draw(const GameTimer& gt) |
这种处理无法完全避免等待的发生
如果GPU速度远大于CPU,,则GPU会处于空闲
反之,CPU会空闲,但是CPU空闲是有利的,因为CPU还需要处理其他算法
采用多帧资源的方法目的是让命令队列一直处于非空状态,即GPU一直有事情可以做
渲染项
单次绘制调用过程中,需要向渲染流水线提交的数据集成为渲染项
我们用一个轻量级的结构体来存储绘制物体所需的数据
1 | struct RenderItem |
渲染过程中所用到的常量数据
- 扩展常量缓冲区
1 | //着色器 |
1 | //与之对应的常量缓冲区数据结构 |
- 刷新常量缓冲区
1 | void ShapesApp::UpdateObjectCBs(const GameTimer& gt) |
1 | void ShapesApp::UpdateMainPassCB(const GameTimer& gt) |
- 着色器改变
1 | VertexOut VS(VertexIn vin) |
- 调整根签名
1 | void ShapesApp::BuildRootSignature() |
不同形状的几何体
GeometryGenerator类
1 | class GeometryGenerator |
细分
1 | void GeometryGenerator::Subdivide(MeshData& meshData) |
中间点的生成
1 | Vertex GeometryGenerator::MidPoint(const Vertex& v0, const Vertex& v1) |
盒子
1 | GeometryGenerator::MeshData GeometryGenerator::CreateBox(float width, float height, float depth, uint32 numSubdivisions) |
球体
1 | GeometryGenerator::MeshData GeometryGenerator::CreateSphere(float radius, uint32 sliceCount, uint32 stackCount) |
柱体网格
1 | GeometryGenerator::MeshData GeometryGenerator::CreateCylinder(float bottomRadius, float topRadius, float height, uint32 sliceCount, uint32 stackCount) |
每一个环最后一个顶点与第一个顶点是重合的,但是纹理坐标不同,只有这样才能保证绘制正确纹理
构建顶部和底部
1 | void GeometryGenerator::BuildCylinderTopCap(float bottomRadius, float topRadius, float height, uint32 sliceCount, uint32 stackCount, MeshData& meshData) |
几何球体
1 | GeometryGenerator::MeshData GeometryGenerator::CreateGeosphere(float radius, uint32 numSubdivisions) |
绘制几何体示例
我们已经创建一个由FrameResource类型元素所构成的向量,每个FrameReousrce中都有上传缓冲区,用于为场景中每一个渲染项存储渲染过程常量和物体常量数据。
如果有3个帧资源和n个渲染项,那么我们需要创建3n个物体常量缓冲区(object constant buffer)以及3个渲染过程常量缓冲区(pass 从constant buffer),因此我们需要创建3(n+1)个常量缓冲区视图。所以我们要修改CBV堆以容纳额外的描述符
1 | #include "../Common/UploadBuffer.h" |
根签名
根参数
1 | typedef struct D3D12_ROOT_PARAMETER |
1 | typedef |
- 描述符表:引用的是描述符堆种的一块连续范围,用于确定要绑定的资源.每个描述符表占用1DWORD
- 跟描述符:通过直接设置跟描述符即可指示要绑定的资源,而且无需将它存于描述符中,但是只有常量缓冲区
CBV
,以及缓冲区的SRV/UAV
才可以根据描述符的身份进行绑定.而纹理SRV
不可以.每个描述符占用2DWORD - 根常量:借助根常量直接绑定一系列的32位的常量值.每个常量32位,占用1个DWORD
描述符表
1 | typedef struct D3D12_DESCRIPTOR_RANGE |
CD3D12_DESCRIPTOR_RANGE
是封装了一些便捷方法,通过Init
方法初始化
如果NumDescriptors
设置为3,BaseShaderRegister
设置为1,类型为CBV,那么对应的HLSL就是
1 | cbuffer cbA : register(b1){};//从1开始 |
- 示例
1 | CD3DX12_DESCRIPTOR_RANGE descRange[3]; |
根描述符
填写D3D12_ROOT_PARAMETER
中的Descriptor
,可以定义为根描述符
1 | typedef struct D3D12_ROOT_DESCRIPTOR |
- ShaderRegister:如果设置为2,类型为CBV,那么对应的常量缓冲区是
register(b2)
- RegisterSpace:空间
与描述符表需要在描述符堆中设置对应的描述符句柄不同,根描述符只需要直接绑定资源的虚拟地址就可以
示例
1 | UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants)); |
根常量
1 | typedef struct D3D12_ROOT_CONSTANTS |
- 示例
1 | CD3DX12_ROOT_PARAMETER slotRootParameter[1]; |
- HLSL代码
1 | cbuffer cbSetting : register(b0) |
与根描述符一i杨,根常量无需涉及描述符堆
根签名复杂示例
- 着色器
1 | Texture2D gDiffuseMap : register(t0); |
- 根签名
1 | CD3DX12_DESCRIPTOR_RANGE texTable; |
陆地与波浪演示程序
栅格坐标
构建一个
m*n
个顶点组成的栅格,意味着具有(m-1)*(n-1)
个四边形,即2倍的三角形如果宽度为
w
,深度为d
,那么x和y轴方向上的间距分别为dx=w/(n-1)
和dz=d/(m-1)
那么第
i
行,第j
列的最坐标为Vij=[-0.5w+j*dx, 0.0, 0.5d-i*dz]
1 | uint32 vertexCount = m*n; |
栅格索引
三角形ABC的索引(i*n+j, i*n+j+1, (i+1)*n+j)
三角形CBD的索引((i+1)*n+j, i*n+j+1, (i+1)*n+j+1)
1 | meshData.Indices32.resize(faceCount*3); // 3 indices per face |
构建山体
1 | void LandAndWavesApp::BuildWavesGeometryBuffers() |
动态顶点缓冲区
动态顶点缓冲区即有可以频繁修改其中顶点数据的资源
如我们可以得到随着时间流走根性三角形的顶点高度,将此数据创建为动态顶点缓冲区
另外比如执行复杂的物理模拟和碰撞检测的粒子系统也需要用到
完整代码
- waves
1 | #pragma once |
1 | #include "Waves.h" |
- LandAndWaves
1 | //*************************************************************************************** |
自定义资源
自定义资源
新建插件StateMachine
,然后手动拷贝一份StateMachineEditor
用于编辑器模块 ,把里面的所有名称改为后缀为Editor版本
然后在Editor
版本的Build.cs
的Private部分中加入如下模块 "UnrealEd", "AssetTools"
,Public部分加入 "StateMachine"
模块
资源类
创建随意UObject
资源,可以保持默认,本例中为UStateMachineAsset
行为类
创建FAssetTypeActions_StateMachine
类继承自FAssetTypeActions_Base
,此类可以接管资源双击打开编辑器的动作,这个类需要在模块启动时注册,稍微会讲
此类建议创建在Editor模块
代码如下
1 | #pragma once |
1 |
|
工厂类
此类最好创建在Editor模块
此类是让引擎认识我们创建的这个资源和自定义标签,似乎定义了以后引擎就自动会识别,即上述两个类的功能
不创建的话连对应的自定义的标签也无法显示
1 | #pragma once |
1 |
|
模块
用于注册自定义标签以及我们之前创建的FAssetTypeActions_StateMachine
类
1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. |
1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. |
然后我们在引擎Content
目录下右键点击就出现我们要的效果
初探UE4反射系统
参考文献
创建项目
AdvanceFunc
创建2个Actor类,
HelloWolrd
,ByeWorld
ByeWorld
的GENERATED_BODY
替换成GENERATED_UCLASS_BODY
GENERATED_BODY/GENERATED_UCLASS_BODY
1 | #define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY); |
1 | #define GENERATED_BODY_LEGACY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY_LEGACY); |
- CURRENT_FILE_ID
定义在 *.generated.h
的底部,如
1 | #undef CURRENT_FILE_ID |
__LINE__
改标准宏指向了该宏使用时候的的函数。加了一个__LINE__宏的目的是为了支持在同一个文件内声明多个类,比如在MyClass.h里接着再声明UMyClass2,就可以支持生成不同的宏名称
对比2个版本的,在generated.h
中发现在UCLASS
版本的52行多出
1 | NO_API AByeWorld(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()) : Super(ObjectInitializer) { }; \ |
所以UCLASS
版本自动申明带FObjectInitializer
参数的构造函数
.generated.h
先看HelloWorld.generated.h
的全貌
1 | #include "UObject/ObjectMacros.h" |
- 关于
GENERATED_BODY
的宏定义
带PRIVATE_PROPERTY_OFFSET和PROLOG
的先跳过,后面再补充
AdvanceFunc_Source_AdvanceFunc_Public_HelloWorld_h_12_INCLASS
和AdvanceFunc_Source_AdvanceFunc_Public_HelloWorld_h_12_INCLASS_NO_PURE_DECLS
的定义一样,如下
1 | #define AdvanceFunc_Source_AdvanceFunc_Public_HelloWorld_h_12_INCLASS_NO_PURE_DECLS \ |
然后是AdvanceFunc_Source_AdvanceFunc_Public_HelloWorld_h_12_ENHANCED_CONSTRUCTORS
和AdvanceFunc_Source_AdvanceFunc_Public_HelloWorld_h_12_STANDARD_CONSTRUCTORS
,对比代码发现
后者多了NO_API AHelloWorld(const FObjectInitializer& ObjectInitializer);
,转身去看一看ByeWorld
发现此版本的2个宏几乎完全一样,只是后者多了Super(ObjectInitializer) { };
这里重点要说的是不管上述宏都将C++11的拷贝构造和移动构造私有化了,目的是防止误操作
DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL
1 | #define DEFINE_DEFAULT_OBJECT_INITIALIZER_CONSTRUCTOR_CALL(TClass) \ |
该声明定义了一个构造函数包装器。需要这么做的原因是,在根据名字反射创建对象的时候,需要调用该类的构造函数。可是类的构造函数并不能用函数指针指向,因此这里就用一个static函数包装一下,变成一个”平凡”的函数指针,而且所有类的签名一致,就可以在UClass里用一个函数指针里保存起来
DECLARE_CLASS
1 | #define DECLARE_CLASS( TClass, TSuperClass, TStaticFlags, TStaticCastFlags, TPackage, TRequiredAPI ) \ |
TClass
:类名TSuperClass
:基类名字TStaticFlags
:类的属性标记,这里是0,表示最默认,不带任何其他属性。读者可以查看EClassFlags
枚举来查看其他定义。TStaticCastFlags
:指定了该类可以转换为哪些类,这里为0表示不能转为那些默认的类,读者可以自己查看EClassCastFlags
声明来查看具体有哪些默认类转换。TPackage
:类所处于的包名,所有的对象都必须处于一个包中,而每个包都具有一个名字,可以通过该名字来查找。这里是”/Script/Hello”,指定是Script下的Hello,Script可以理解为用户自己的实现,不管是C++还是蓝图,都可以看作是引擎外的一种脚本,当然用这个名字也肯定有UE3时代UnrealScript
的影子。Hello就是项目名字,该项目下定义的对象处于该包中。Package的概念涉及到后续Object的组织方式,目前可以简单理解为一个大的Object包含了其他子Object。TRequiredAPI
:就是用来Dll导入导出的标记,这里是NO_API,因为是最终exe,不需要导出。
.generated.cpp
1 |
|
IMPLEMENT_CLASS
1 | #define IMPLEMENT_CLASS(TClass, TClassCrc) \ |
目的是把该类的信息传进去给GetPrivateStaticClassBody
函数,该函数真正创建UClass*
UPROPERTY,UFUNCTION
在
ByeWorld
里加入属性和函数
1 | UPROPERTY(BlueprintReadWrite) |
对比两文件以后,发生改变的地方如下
1 | #define AdvanceFunc_Source_AdvanceFunc_Public_ByeWorld_h_12_RPC_WRAPPERS \ |
1 | #define AdvanceFunc_Source_AdvanceFunc_Public_ByeWorld_h_12_RPC_WRAPPERS_NO_PURE_DECLS \ |
1 | #define AdvanceFunc_Source_AdvanceFunc_Public_ByeWorld_h_12_EVENT_PARMS \ |
源于蓝图虚拟机的约定,蓝图调用的函数前面会加上exec
- .generated.cpp
1 | ADVANCEFUNC_API UFunction* Z_Construct_UFunction_AByeWorld_BP_Callable(); |
总结一下
通过
ConstructUFunction
把对应的函数反射数据加入到反射系统然后包括函数和属性等反射数据也加入到UCLASS一起通过
ConstructUClass
加入到反射系统即通过类的反射数据可以找到类的所有成员,通过函数的反射数据找到函数的成员
C++11新特性
auto
- 自动类型推导
1 | std::vector<std::string> vec; |
- 定义模板函数时,推到依赖模板函数的变量类型
1 | template<_typename _Tx, _typename _Ty> |
- 返回值
1 | auto multipy(_Tx x, _Ty y)->decltype(x * y) |
- 其他
1 | auto a=10,b=11.0;//报错,初始化必须统一类型 |
1 | int a=1; |
1 | int arr[3] = {1, 2, 3}; |
decltype
从变量或者表达式获取类型
1 | int var; |
1 | decltype(var);//int |
对于
decltype
所用的引用来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有所不同。如果decltype
使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。
C++14可以使用不带尾随返回类型的 decltype(auto)
来声明其返回类型取决于其模板参数类型的模板函数。
1 | template<typename T, typename U> |
using
- 命名空间
1 | using namespace std; |
- 定义别名,类似
typedef
1 | using itType=std::vector<std::string>::iterator; |
using与typedef的差别是 using可以用于模板部分具体化,但是typedef不能
1 | template<class T> |
- 当一个派生类私有继承基类时,基类的public和protected数据成员在派生类中是private的形式,如果想让这些继承而来的数据成员作为public或者protected成员,可以用using来重新声明。using声明语句中名字的访问权限由该using声明语句之前的访问说明符决定。
1 | class Basic{ |
- 关于重写重载函数
1 | class A1 |
nullptr
nullptr
比0更安全
枚举作用域
1 | enum EnumA |
所以我们需要设置枚举的作用域,这样才可以编译的过
1 | enum class EnumA |
Lambda
其实lambda实现的方法是创建一个简略的类。**这个类重载了operator()**,所以表现的像个普通函数。一个lambda函数是这个类的实例。当这个类构造的时候,所有捕获的变量被传送到类中并保存为成员变量。
表达式
1 | [capture](parameters)->return-type{body} |
几个例子
1 | [](int x, int y) { return x + y; } // 隐式返回类型 |
- 关于
[]
的捕获信息
1 | [] //未定义变量.试图在Lambda内使用任何外部变量都是错误的. |
示例
1 | A1* a1=new A1(); |
explicit
显式转换,禁止单参数构造函数导致的自动转换
1 | class plebe |
default,delete
- default
4类特殊函数可以用default,即默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符
特殊情况,如果类内有指针成员,特殊函数都用default,以下情况会导致错误
1 | class testA |
- delete
类内成员初始化
1 | class se |
右值引用
- 左值、右值
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)
- 右值、将亡值
C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。
将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
- 左值引用、右值引用
- 左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在
- 右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化
- 引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
1 | //int &a =2;//左值引用绑定右值,fail |
智能指针
智能指针是用对象去管理一个资源指针,同时用一个计数器计算引用当前指针对象的个数,当管理指针的对象增加或减少时,计数器也相应加1或减1,当最后一个指针管理对象销毁时,计数器为1,此时在销毁指针管理对象的同时,也对指针管理对象所管理的指针进行delete操作。
- shared_ptr
std::shared_ptr包装了new操作符动态分配的内存,可以自由拷贝复制,基本上是使用最多的一个智能指针类型。
注意事项
我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。
1 | // 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值 |
禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。
1 | shared_ptr<int>sp = make_shared<int>(10); |
- weak_ptr
与std::shared_ptr最大的差别是在赋值的时候,不会引起智能指针计数增加。
1 | class A { |
weak_ptr的一些用法
1 | weak_ptr<T> w; //空weak_ptr可以指向类型为T的对象 |
- unique_ptr
unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效
1 | unique_ptr<int> p1=make_unique<int>(11); |
模板和STL方面的修改
基于范围的for循环
1 | double price[5]={1.1,2.2,3.3,4.4,5.5}; |
新的STL容器
C++11新增的容器:forward_list,unordered_map,unordered_multimap,unordered_set,unordered_multiset
- 关于unordered_map与map
两者的接口差不多,基本可以互换。
一般来说unordered_map的综合性能比map要好,因此,通常我们可以使用unorderd_map代替map。
以下情况推荐使用map:
关键字类型的hash函数设计的很差, 或者==运算符的性能极差, 导致hash过程太耗时;
对内存使用有严格要求, 不能接受存储hash table的额外内存开销;
元素要求按顺序存储, 或者常常需要关联访问一个元素的上一个/下一个元素, 或者需要遍历整个map。
新的STL方法
新增了STL方法cbegin()
和cend()
,这些方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可以用于指定包含全部元素的区间;这些新方法将元素视为const。类似的,crbegin()和crend()是rbegin()和rend()的const版本
valarray升级
C++11添加了两个函数begin()和end(),都接受valarray作为参数并且返回迭代器
摒弃export
C++98增加了关键字export,C++11摒弃了这个特性但是保留了关键字export
尖括号
为了避免与运算符>>混淆,C++要求在申明嵌套模板时使用空格将尖括号分开
1 | vector<list<int>> vl;//C++11开始不再强求空格 |
DX学习笔记(四):绘制几何体
用DirectX绘制几何体
流程简述
顶点与输入布局
- 创建顶点结构体
- 设置输入布局描述
D3D12_INPUT_ELEMENT_DESC
顶点/索引缓冲区
- 创建顶点数据/索引数据
- 创建缓冲区/索引缓冲区
顶点着色/像素着色
- 编写HLSL着色器文件
- 编译着色器
常量缓冲区(上传堆,CPU可写,GPU可读)
- 填充常量缓冲区所需的描述符(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
) - 构造缓冲区对象(单独的类),该类创建常量缓冲区
- 设置缓冲区大小为256B的整数倍
- 创建常量缓冲区视图
- 创建根签名
后续在提交命令列表的时候讲根签名和常量缓冲区绑定到流水线
流水线状态对象 PSO
- 创建PSO对象
D3D12_GRAPHICS_PIPELINE_STATE_DESC
- 设置PSO对象的各类参数,包括输入布局描述符、根签名、PS、VS、光栅器对象等
- 通过
md3dDevice->CreateGraphicsPipelineState(&psoDesc,IID_PPV_ARGS(&mPSO))
创建PSO
构建几何体
- 顶点/索引数据,布局描述
- 创建Geo类,并且设置类内参数,这些参数在填充视口描述的时候需要
- 创建顶点/索引缓冲区
- 在绘制阶段通过命令列表将顶点和索引视图绑定到流水线插槽
- 通过
DrawIndexedInstanced
绘制几何体
刷新(update)
- 根据鼠标信息重新构建观察矩阵(相对坐标的变换)
- 刷新常量缓冲区
Resize
- 重新构建透视投影矩阵
- 然后反馈到Update的常量缓冲区上
世界矩阵随移动/旋转/缩放而改变
观察矩阵随虚拟摄像机的移动/旋转/缩放而改变
投影矩阵随窗口大小调整而改变
刷新的数据通过根签名设置到着色器
顶点与输入布局
- 顶点结构体
1 | struct Vertex |
1 | struct Vertex2 |
- 输入布局描述
1 | typedef struct D3D12_INPUT_LAYOUT_DESC |
1 | typedef struct D3D12_INPUT_ELEMENT_DESC |
- SemanticName:语义,用于将顶点结构体中的元素与顶点着色器输入签名(参数列表)中的元素映射起来
- SemanticIndex:附加到语义的索引。
- Format:顶点元素格式,如
DXGI_FORMAT_R32_FLOAT
1D32位浮点标量 - InputSlot:传递元素所用的输入槽,DirectX12一共16个,索引0-15,一般用同一个输入槽即输入槽0
- AlignedByteOffset:顶点结构体的首地址到某点元素起始地址的偏移量
1 | struct Vertex2 |
- InputSlotClass:一般指定为
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA
,另外一种D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA
用于实现实例化的高级技术 - InstanceDataStepRate:一般设置0,如果采用实例化就用1
- 示例
1 | D3D12_INPUT_ELEMENT_DESC desc1 = |
1 | D3D12_INPUT_ELEMENT_DESC desc2 = |
顶点缓冲区
为了使GPU可以访问顶点数组,就需要把它们放置在缓冲区
的GPU资源(ID2D12Resource)
顶点缓冲区:存储顶点的缓冲区
缓冲区描述:
CD3D12_RESOURCE_DESC
1 | struct CD3DX12_RESOURCE_DESC : public D3D12_RESOURCE_DESC |
此描述继承自D3D_RESOURCE_DESC
,重点实现了几个比较便捷的方法如
1 | static inline CD3DX12_RESOURCE_DESC Buffer( |
其他还有比如CD3DX12_RESOURCE_DESC::Tex1D,CD3DX12_RESOURCE_DESC::2D
等便捷方法
然后通过ID3D12Device::CreateCommittedResource
方法创建ID3D12Resource
对象
- 示例:创建默认缓冲区函数申明
1 | // Create the actual default buffer resource. |
DX12中所有资源都用
ID3D12Resource
接口表示,DX11用各种比如ID3D11Buffers
等表示
为了优化性能,静态几何体的顶点缓冲区置于默认堆中
因为CPU不能向默认堆(D3D12_HEAP_TYPE_DEFAULT
)中的顶点缓冲区写入数据,所以就需要用到一个中介
上传缓冲区
为了将数据从CPU复制到GPU显存中,即将顶点数据从系统内存复制到上传缓冲区,然后再复制到真正的顶点缓冲区中示例:创建默认缓冲区函数实现
1 | Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer( |
- D3D12_SUBRESOURCE_DATA
1 | typedef struct D3D12_SUBRESOURCE_DATA |
- 示例:创建有立方体8个顶点的默认缓冲区
1 | std::array<Vertex, 8> vertices = |
- 顶点缓冲区视图:为了将顶点缓冲区绑定到渲染流水线
1 | typedef struct D3D12_VERTEX_BUFFER_VIEW |
- BufferLocation:顶点缓冲区的虚拟地址,通过
ID3D12Resource::GetGPUVirtualAddress
方法得到此地址 - SizeInBytes:顶点缓冲区大小(字节)
- StrideInBytes:每个顶点元素占用的字节
然后将顶点缓冲区视图和渲染流水线上的一个输入槽绑定
1 | //第一个参数:所用的输入槽,一般用0 |
将顶点缓冲区置入输入槽并不会进行绘制操作,仅仅是为顶点数据送至渲染流水线做好准备
真正绘制顶点的方法
1 | //参数1:每个实例绘制的顶点数 |
索引和索引缓冲区
- 索引缓冲区
与顶点类似,为了使GPU可以访问索引数组,需要将它们放置于GPU的缓冲区资源内,此缓冲区即索引缓冲区
同样用CreateDefaultBuffer
方法创建索引缓冲区
索引缓冲区用到结构体D3D12_INDEX_BUFFER_VIEW
描述
1 | typedef struct D3D12_INDEX_BUFFER_VIEW |
同顶点缓冲区,也需要绑定到渲染流水线,通过ID3D12GraphicsCommandList::IASetIndexBuffer
方法将索引缓冲区绑定到输入装配器阶段
1 | std::array<std::uint16_t, 36> indices = |
使用索引时,要用
ID3D12GraphicsCommandList::DrawIndexedInstanced
代替DrawInstanced
方法
1 | //第4个参数:读取顶点之前,要为每个索引都加上的整数值 |
将多个顶点缓冲区和索引缓冲区合并以后会需要用到第4个参数
1 | mCommandList->DrawIndexedInstanced(numOfBox, 1, 0, 0, 0); |
顶点着色器示例
- 高级着色器语言:HLSL
示例
1 | cbuffer cbPerObject:register(b0) |
HLSL没有指针和引用,用out
表示输出参数
HLSL所有函数都是lnline函数
参数POSITION
和COLOR
语义将元素映射到顶点着色器对应的输入参数;同理输出参数的语义将参数映射到下一个阶段(几何着色器或者像素着色器)对应的输入参数;
SV_POSTION
的SV
代表系统值,此数据存有齐次裁剪空间顶点信息,指定了此语义以后使GPU进行如裁剪、深度测试等处理时,实现其他属性无法介入的有关运算
任何没有系统修饰的参数,都可以根据需求以合法的语义修饰
可以把函数的参数封装成结构体,下面是另一种实现方式
1 | struct VertexIn |
如果没有使用
几何着色器
,那么顶点着色器必须用SV_POSITION
来输出顶点在齐次裁剪空间的位置如果使用了,可以把得到齐次裁剪空间位置的工作交给它来处理
顶点着色器/几何着色器无法完成透视除法,此阶段只能完成投影矩阵运算,透视除法由后续硬件执行
布局描述符与签名匹配问题
ID3D12Device::CreateGraphicsPipelineState
函数时,需要指定布局描述符和顶点着色器,这就涉及到参数匹配问题
- 不匹配
1 | struct Vertex |
- 匹配
1 | struct Vertex |
- 匹配
1 | struct Vertex |
像素着色器示例
光栅化阶段先对顶点着色器进行插值计算,然后把数据传递至像素着色器作为输出
像素着色器针对每一个像素片段而运行的函数,但是部分像素片段不会传入或者留存在后台缓冲区,会在深度测试等情况被丢弃掉;即像素是最终写入后台缓冲区的数据,像素片段是候选像素
示例
1 | cbuffer cbPerObject:register(b0) |
参数列表后的SV_TARGET
表示该返回值的类型樱岛与渲染目标格式相匹配(render target format
)1
同样可以利用输入输出结构体来重写上述代码
1 | struct VertexIn |
常量缓冲区
创建常量缓冲区
常量缓冲区也是一种GPU资源ID3D12Resource
1 | cbuffer cbPerObject:register(b0) |
此代码中的cbuffer
对象(常量缓冲区)的名称就是cbPerObject
,存储一个4x4矩阵gWorldViewProj
此矩阵表示把一个点从局部空间变换到齐次裁剪空间所用到的由世界、视图和投影3种变换组合而成的矩阵
与顶点和索引缓冲区不同,从常量缓冲区由CPU每帧更新顶一次
常量缓冲区创建到上传堆而非默认堆,使得我们从CPU端更新常量
常量缓冲区的硬件有特别要求,大小必须为256B的整数倍
示例
1 | struct ObjectConstants |
mUploadCBuffer
存储了一个ObjectConstants
类型的常量缓冲区数组
绘制物体时,只要将常量缓冲区视图CBV
绑定到那个存有物体相应常量缓冲区的子区域即可
- 着色器模型5.1
1 | struct ObjectConstants |
常量缓冲区的数据元素被定义在一个单独的结构体中,随后用这个结构来创建一个常量缓冲区,然后就可以在着色器里访问常量缓冲区中的各个字段
1 | uint index = gObjConstants.matIndex; |
着色器模型定义了HLSL的编写规范,确定了其内置函数、着色器属性等一切语言元素
更新常量缓冲区
常量缓冲区是用D3D12_HEAP_TYPE_UPLOAD
类型创建的,所以可以用CPU为常量缓冲区更新数据
1 | Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer; |
1 | memcpy(mMappedData,&data,dataSizeInBytes);//用此函数将数据从系统内存复制到常量缓冲区 |
等更新完成,然后在释放映射内存之前对其进行Unmap(取消映射)
操作
1 | if(mUploadBuffer != nullptr) |
Unmap
的第一个参数是子资源索引,第二个是指向D3D12_RANGE
结构体的指针,描述取消映射的内存范围,如空则整个资源映射。
上传缓冲区辅助函数
- UploadBuffer类
1 | template<typename T> |
世界矩阵随移动/旋转/缩放而改变
观察矩阵随虚拟摄像机的移动/旋转/缩放而改变
投影矩阵随窗口大小调整而改变
- OnMouseMove
1 | void BoxApp::OnMouseMove(WPARAM btnState, int x, int y) |
- Update
1 | void BoxApp::Update(const GameTimer& gt) |
常量缓冲区描述符
常量缓冲区描述符都存放在以D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV
类型的描述符堆里,这个堆混合存储了常量缓冲区、着色器资源和无序访问描述符
1 | D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc; |
在创建着色器程序访问的描述符时,需要把Flags指定为
DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE
然后通过如下代码创建常量缓冲区
1 | void BoxApp::BuildConstantBuffers() |
根签名和描述符表
- 根签名
1 | //将纹理资源绑定到纹理寄存器槽0 |
register(*#)
中的*
表示寄存器传递的资源类型。t:着色器资源视图;
s:采样器
u:无序访问视图
b:常量缓冲区视图
在执行绘制命令之前,那些应用程序将绑定到渲染流水线上的资源,它们会被映射到着色器的对应输入寄存器。
根签名一定要与使用它的着色器相兼容,在创建流水线状态对象时会对此进行验证
不同的绘制调用可能会用到一组不同的着色器程序,意味着要用到不同的根签名
在Direct3D中,根签名由ID3D12RootSignature
接口表示,并用一组描述绘制调用过程中着色器所需资源的根参数
定义而成。
根参数可以是根常量
、根描述符
或者描述符表
如果把着色器程序看成是一个大函数,那顶点数据和常量数据就是从CPU传入着色器函数的参数,而根签名就好比这些参数的函数签名。所以根签名其实就是将着色器需要用到的数据绑定到对应的寄存器槽上,供着色器访问。
示例
1 | oid BoxApp::BuildRootSignature() |
1 | // Root parameter can be a table, root descriptor or root constants. |
上述代码创建了一个根参数,目的是将含有一个CBV
的描述符表绑定到常量缓冲区寄存器0,即HLSL
代码中的register(b0)
根签名至定义了应用程序要绑定到渲染流水线的资源,没有真正的执行任何资源绑定操作
只有通过命令列表设置根签名,才可以用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable
方法令描述符表与渲染流水线绑定
1 | mCommandList->SetGraphicsRootDescriptorTable(0, //将根参数按此索引进行设置 |
下列代码先将根签名和CBV堆设置到命令列表上,并随后通过设置描述符表来指定我们希望绑定到渲染流水线的资源
1 | mCommandList->SetGraphicsRootSignature(mRootSignature.Get()); |
·
编译着色器
着色器程序必须先被编译为一种可移植的字节码,接下来图形驱动程序将获取这些字节码,并将其重新便以为针对当前系统GPU所优化的本地指令[ATI1]
可以用如下函数在运行期间对着色器进行编译
1 | HRESULT D3DCompileFromFile( |
- ID3DBlob
一段普通的内存块,有2个接口方法
LPVOID GetBufferPointer
:返回对象中数据的void*
类型的指针,使用之前需要转换为适当类型SIZE_T GetBufferSize
:返回缓冲区字节大小
- 运行时编译着色器函数
1 | ComPtr<ID3DBlob> CompileShader( |
离线编译
离线编译的原因
- 对于复杂的着色器,编译耗时太长,因此借助离线编译即可缩短应用程序的加载时间
- 以便在早于运行时的构建处理期间提早发现编译错误
- 对于Windows9应用商店的应用而言,必须采取离线编译这种方式
一般用
.cso
为已编译的着色器对象的扩展名
使用DirectX自带的FXC
命令行编译工具编译
- 如用CMD输入代码编译
1 | C:\Program Files (x86)\Windows Kits\10\bin\x86>fxc.exe C:\Users\Administrator\Desktop\color.hlsl /T vs_5_0 /Fo "color.cso" /E "VS" |
成功输出color.cso
文件
compilation object save succeeded; see C:\Program Files (x86)\Windows Kits\10\bin\x86\color.cso
- 常用的编译参数
参数 | 描述 |
---|---|
/Od | 禁用优化(利于调试) |
/Zi | 开启调试信息 |
/T “string” | 着色器模型版本,如输入vs_5_0 |
/E “string” | 着色器入口,如案例中的VS ,PS |
/Fo “string” | 经过编译的着色器对象字节码 |
/Fc “string” | 输出一个着色器汇编语言清单(调试、查阅细节) |
其他清单可以参考微软SDK文档
光栅器状态
光栅器状态只接受配置,非可编程,由结构体D3D12_RASTERIZER_DESC
表示
1 | typedef struct D3D12_RASTERIZER_DESC |
FillMode:默认是实体渲染,如果用D3D12_FILL_MODE_WIREFRAME
则是线框渲染
CullMode:默认剔除背面,D3D12_CULL_MODE_NONE
不剔除,D3D12_CULL_MODE_FRONT
剔除正面
FrontCounterClockwise:默认false,根据观察视角,将定点顺序为顺时针方向的三角形看作正面;如true则相反
示例
1 | CD3DX12_RASTERIZER_DESC rsDesc(D3D12_DEFAULT); |
CD3DX12_RASTERIZER_DESC
是扩展自D3DX12_RASTERIZER_DESC
的结构,添加了一些辅助构造函数的工具类,如接受CD3DX12_DEFAULT
参数的构造函数。
CD3DX12_DEFAULT
是一个哑类型(dummy),将需要被初始化的成员重载为默认值
1 | struct CD3DX12_DEFAULT {}; |
流水线状态对象
ID3D12PipelineState
表示流水线状态对象(PSO)
结构体
1 | typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC |
示例
1 | D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc; |
PSO的验证和创建过于耗时,所以一般在初始化期间就生成PSO
视口和裁剪矩形等属性独立于PSO
用不同的PSO绘制不同的物体
1 | mCommandList->Reset(mDirectCmdListAlloc.Get(),mPSO1.Get()); |
几何图形辅助结构体
此结构体定义了MeshGeometry中存储的单个几何体
1 | struct SubmeshGeometry |
1 | struct MeshGeometry |
绘制Box
1 | #include <Windows.h> |
DX学习笔记(三):渲染流水线
渲染流水线
平行线最终会相交于消失点
,又称为灭点
物体重叠
:即不同命的物体能够遮挡住其后侧物体的局部
3D实体对象是通过三角形网格
来近似表示的
颜色
分量式乘法:(r,g,b)*(a,b,c)=(ra,gb,bc)
128位颜色
每个分量用浮点表示,即4D向量 (r,g,b,a)
0<=r,g,b,a<=1
DirectXMath
库对分量式乘法的支持
1 | XMVECTOR XM_CAKKCIBV XMColorModulate (FXMVECTOR C1,FXMVECTOR C2);//返回C1*C2 |
32位颜色
每个分量仅分配1个字节,因此每个占用8字节的颜色分量就分别描述256种不同的颜色强度
即0代表无强度,256代表最大强度
DirectXMath
库(include<DirectXPackedVector.h>)
在DirectX::PackedVector
命名空间提供了相应函数
1 | namespace DirectX |
通过将[0,255]
映射到[0,1]
即可完成32位到128位的转换
转换函数
1 | XMVECTOR XM_CALLCONV XMLoadColor( const XMCOLOR* pSource); |
1 | void XM_CALLCONV XMStoreColor( XMCOLOR* pDestination, FXMVECTOR V ); |
渲染流水线概述
渲染流水线(rendering pipeline)是以摄像机位观察视角而生成的2D图像的一系列完整步骤
输入装配阶段
输入装配阶段:从显存中读取集合数据(顶点和索引,vertex and index)
再装配为几何图元(几何基元),如三角形和线条
- 顶点缓冲区:将顶点与渲染流水线绑定的特殊结构体
图元拓扑(primitive topology)
1 | virtual void STDMETHODCALLTYPE IASetPrimitiveTopology( |
1 | typedef |
示例
1 | //通过线列表来绘制对象 |
- 点列表
D3D_PRIMITIVE_TOPOLOGY_POINTLIST
- 线条带
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP
- 线列表
D3D_PRIMITIVE_TOPOLOGY_LINELIST
- 三角形带
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
- 剔除(culling)问题:奇数与偶数三角形的绕序(环绕顺序)即装配图元的顶点顺序是不同的
CPU会对偶数三角形的前两个顶点
顺序进行调换已达到有奇数三角形绕序相同
正确的应该是后两个顶点顺序调换,而OpenGL中才是前两个顶点调换
- 三角形列表
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST
三角形列表与三角形带的区别是,三角形列表的三角形可以彼此分离,所以每n*3个顶点组成n个三角形
- 具有邻接数据的图元拓扑
对于有邻接数据的三角形列表,每个三角形都有3个与之相邻的邻接三角形
借助顶点缓冲区
和索引缓冲区
将它们随主三角形一并提交至渲染流水线
并且要把拓扑类型指定为D3D12_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ
让渲染流水线知道如何以顶点缓冲区的顶点来构建主三角形和其邻接三角形;
邻接图元的顶点只作为集合着色器的输入数据,并不会被绘制
索引
1 | Vertex quad[6]={ |
- 绕序:为三角形指定顶点顺序
用顶点列表
和索引列表
组合起来构成三角形
如
1 | Vertex v[9]={v0,v1,v2,v3,v4,v5,v6,v7,v8}; |
顶点着色阶段
- 世界变换:将局部坐标系内的坐标转换到世界坐标系
- 世界矩阵:上述变换的矩阵
$$
W=SRT
$$
- 观察空间:相机的局部坐标系,亦称视觉空间、摄像机空间等
- 取景变换:世界空间至观察空间的变换;此变换的矩阵叫做
观察矩阵
观察矩阵函数
1 | inline XMMATRIX XM_CALLCONV XMMatrixLookAtLH |
示例
1 | XMVECTOR eyePos=XMVectorSet(100,100,100,1); |
- 顶点的投影线:顶点到观察点的连线
- 透视投影变换:3D顶点v变换至其投影线与3D投影平面的交点v、
近平面(近裁剪面)
n
,远平面f
,垂直视场角α
,纵横比r
,4个参数定义了以原点作为投影的中心,以Z
轴进行观察的平截头体
纵横比
$$
r=w/h
$$
w:投影窗口的宽度
h:投影窗口的高度
如果后台缓冲区与投影窗口纵横比不一致,在映射的过程中会产生非均匀缩放(不等比例缩放),导致图像拉伸
NDC:规格化设备坐标(Normalized Device Coordinates)
透视除法(齐次除法):顶点与投影矩阵相乘以后,对每个坐标除以
w=z
归一化深度值:通过函数
g(z)
把z
坐标从[n,f]
映射到区间[0,1]
$$
g(z)=A+ {B \over z}
$$
- 透视投影矩阵
$$
P=\begin{bmatrix}1\over rtan({\alpha \over 2}) & 0 & 0 & 0\
0 & 1\over{tan({\alpha\over 2})} & 0 & 0 \
0 & 0 & f\over {f-n} & 1 \
0 & & 0 & -nf\over{f-n} & 0
\end{bmatrix}
$$
在顶点乘以投影矩阵后,集合体会处于所谓的
齐次裁剪空间
或者投影空间
完成透视除法以后,便是**规格化设备坐标(NDC)**了
- XMMatrixPerspectiveFovLH
1 | inline XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH |
示例
1 | XMMATRIX M2=XMMatrixPerspectiveFovLH(0.25f*XM_PI,1092/1080,1.0,1000); |
2.41421 0 0 0
0 2.41421 0 0
0 0 1.001 1
0 0 -1.001 0
曲面细分阶段
利用镶嵌化处理技术对网格中的三角形进行细分,以此来增加物体表面上的三角形数量。
再将这些新增的三角形偏移到适当的位置,使网格表现出更加细腻的细节
- 曲面细分的优点
- 实现LOD
- 在内存中只维护低模,在有需求时动态增添三角形,从而节省资源
- 处理动画和物理时使用低模,渲染时使用处理过的高模
几何着色器阶段
几何着色器阶段接受输入是完整的图元
与顶点着色器相比,几何着色器可以创建或者修改几何体
比如,将一个点或者一条线扩展为一个四边形
裁剪
对视锥体之外的物体进行裁剪
苏泽兰-霍奇曼裁剪算法
光栅化阶段
光栅化阶段(RS,栅格化):投影主屏幕上的3D三角形计算出对应的像素颜色
- 视口变换:裁剪完成后,硬件通过透视除法将物体从齐次空间变换为规格化设备坐标(NDC)。此后顶点
x、y
坐标会以像素单位表示 - 背面剔除:将背面朝向的三角形从渲染流水线中除去
- 透视矫正插值:为得到2D空间的顶点的插值属性,对3D空间的三角形属性进行线性插值,即利用三角形属性计算出内部像素的属性
- 像素着色器阶段(pixel shader):针对每个像素片段进行处理,根据顶点的差值属性作为输入来计算像素颜色,亦可实现如逐像素光照(per-pixel lighting)、反射以及阴影等复杂效果
- 输入合并阶段:上述阶段生成的像素片段送至渲染流水线的输出合并阶段(OM)。丢弃部分像素(如未通过深度测试等),剩下的写入后台缓冲区,执行混合(blend)操作(透明效果也混合实现的)
DX学习笔记(一):数学基础
向量代数
向量
DirectX3D采用的左手坐标系
- 左手坐标系:伸出左手,手指方向对准X轴正方向,弯曲手指对象Y轴正方向,大拇指指的就是Z轴正方向
向量的基本运算
两个向量相等。即u=v。当且仅当u和v的每个分量相等,即ux=vx,uy=vy, uz=vz
向量的加法即两个向量对应分量都相加
向量与标量相乘即每一个分量与标量相乘
向量减法与加法类似
向量加法的几何意义u+v,即u的尾部与v的头部重合
向量的长度和单位向量
- 3D向量的模可以用2次毕达哥拉斯定理(勾股定理)计算得到
- 一个向量的长度变为单位长度称为向量的规范化(normalizing),具体实现方法是
点积
- 点积(dot product)是一种计算结果为标量值的向量乘法
- 如果u·v=0,那么u和v正交(垂直)
- 如果u·v>0,那么夹角为锐角,即小于90°
- 如果>0,那么夹角为钝角
正交投影
即向量p是v在n向量上的投影
一般的投影公式如下
正交化
如果向量集里面所有向量都相互正交,那么此向量集是规范正交
格拉姆-施密特正交化
从文字上描述即,将向量v添加到规范正交集中时,需要用v减去这个规范正交集中的所有其他向量{w1,w2…}方向上的分量投影,这样确保新加入的v与该集合中的其他放量相互正交
- 假设有向量集{v0,v1},将其规范正交至集{w0,w1}
则需要进行如下操作
- 如果是三维向量集{v0,v1,v2},至规范正交集{w0,w1,w2}
则进行如下操作
叉积
叉积公式
两个向量叉积的意义即得到正交于两个向量的向量,采用的左手坐标系,即手指指向一个向量,通过向内弯曲小于等于180°的角度后到达另外一个向量,则大拇指方向是叉积所得向量的方向
用叉积类规范正交化
点
点(x,y,z)即从原点至该点的向量
用DirectXMath库
DirectXMath变量的使用规范
- 局部变量或全局变量用XMVECTOR类型
- 对于类中的数据成员,用XMFLOAT2\XMFLOAT3和XMFLOAT4类型
- 在运算之前,通过加载函数将XMFLOATn类型转换成XMVECTOR类型
- 用XMVECTOR实例来进行运算
- 通过存储函数将XMVECTOR类型转换为XMFLOATn类型
加载方法和存储方法
1 | //将数据从XMFLOAT2类型加载到XMVECTOR类型 |
参数传递
传递规则
- 前三个 XMVECTOR 参数用类型
FXMVECTOR
- 第四个 XMVECTOR 参数用
GXMVECTOR
- 第5、6个 XMVECTOR 参数用
HXMVECTOR
- 其余的 XMVECTOR 参数用
CXMVECTOR
在32位windows系统,编译器将根据**_fastcall**调用约定将前3个XMVECTOR参数传递到寄存器,其余放到栈
在32位windows系统,编译器将根据**_vectorcall**调用约定将前6个XMVECTOR参数传递到寄存器,其余放到栈上
其余平台上的定义,可以参见
DirectXMath
库文档中的Library Internals
下的Calling Converntions
部分的[DirectXMath]
常向量
XMVECTOR类型的常量实例用 XMVECTORF32
表示
1 | static const XMVECTORF32 v1 = { 0.5f,1.0f,1.0f,1.0f }; |
XMVECTORF32是一种16字节对齐的结构体,数学库中有将他转换至XMVECTOR类型的运算符
另外也可以用XMVECTORU32
类型来创建由证书数据构成的XMVECTOR常向量
1 | static const XMVECTORU32 v2 = { 1,2,3,4 }; |
重载运算符
XMVECTOR类型针对向量的加法、减法和标量乘法都重载了运算符,如下
其他
DirectXMath库定义了一组于PI有关的常用数学常量近似值
另外还有角度和弧度之间的转化以及比较大小的函数
Setter函数
DirectXMath库提供了下列函数,用来设置XMVECTOR类型中的数据
1 | //返回零向量 |
使用案例
1 | #include <DirectXMath.h> |
向量函数
1 | XMVECTOR v1 = XMVectorSet(1, 1, 1, 4); |
浮点数的误差
1 | XMVector Epsilon={0.1,0.1,0.1,0.1}; |
矩阵代数
定义
m*n的矩阵M,即由m行,n列构成的矩形阵列
- A1.*
- A*.1
乘法
- 矩阵AxB乘法的前提条件:A的列数必须于B的行数相同
乘法公式
- 即矩阵A的第i个行向量于B的第j个列向量点积
- 行列数不相等的矩阵乘法不满足交换律即 AB≠BA
向量于矩阵乘法
转置矩阵
转置矩阵(transpose matrix)是将原矩阵阵列的 行与列进行互换 即mn的矩阵变成nm的矩阵
**M **的转置矩阵记作 Mt
- 转置矩阵具有下列性质
单位矩阵
单位矩阵(identity matrix)是一种主对角线上的元素都是1,其他元素都是0的方阵,如
- 任何单位与单位矩阵相乘,得到的依然是原来的矩阵,而且满足交换律
MI=IM=M
矩阵的行列式
行列式记作: det A
- 当且仅当det A≠0时,方阵A是可逆的
余子阵
n x n的矩阵A,余子阵(minor matrix) 即从A中去除第 i 行和第 j 列的**(n-1)*(n-1)**矩阵
行列式
- 对于2 x 2 的矩阵来说,行列式公式为:
- 3 x 3的矩阵,行列式公式为:
- 4 x 4的矩阵,行列式公式为:
- 案例:
一个矩阵的行列式就是一个平行多面体的(定向的)体积,这个多面体的每条边对应着对应矩阵的列。如果学生得知了这个秘密(在纯粹的代数式的教育中,这个秘密被仔细的隐藏了起来),那么行列式的整个理论都将成为多重线性形式理论的一部分。倘若用别的方式来定义行列式,任何敏感的人都将会永远痛恨诸如行列式,Jacobian式,以及隐函数定理这些东西。
——俄国数学家阿诺尔德(Vladimir Arnold)《论数学教育》
伴随矩阵
如果A中的每个元素分别计算出Cij,并将它至于矩阵CA中的第 i 行,第 j 列,那么将获得矩阵A的 代数余子式矩阵(cofactor matrix of A)
取矩阵CA的转置矩阵,得到矩阵A的伴随矩阵(adjoint matrix of A),记作
逆矩阵
- 只有方阵才有逆矩阵
- n x n矩阵M的逆也是一个n x n的矩阵,记作 M-1
- 不是每个方阵都有逆矩阵,存在逆矩阵的方阵称为可逆矩阵(invertible matrix),不存在逆矩阵的叫做奇异矩阵(singular matrix)
- 可逆矩阵的逆矩阵是唯一的
- 可逆矩阵有逆矩阵相乘等到单位方阵: MM-1=M-1M=I,矩阵与其逆矩阵妈祖交换律
逆矩阵的推导公式
- 案例,
DirectXMath库处理矩阵阵列
1 | ostream& XM_CALLCONV operator<<(ostream& os, FXMVECTOR v) |
变换
线性变换
线性变换函数满足
线性变换函数公式
矩阵表示法
**u=(x,y,z)**也可以写作
矩阵表示法公式
R3,标准基向量
i=(1,0,0), j=(0,1,0), k=(0,0,1)
矩阵表示法公式2
根据线性组合,上述公式可以改写成
线性变换的矩阵表示法
我们称矩阵 A是线性变换的矩阵表示法
缩放
定义
- S就是一种线性变换
缩放变换的矩阵表达式
- 缩放矩阵的逆矩阵
- 案例
旋转
旋转矩阵基础公式
上述公式里,c=cosθ 而且 s=sinθ
旋转矩阵有个特性:每个行向量都为单位长度且两两正交,即这些行向量都是规范正交(orthonormal)
若一个矩阵的行向量都是规范正交的,则此矩阵为正交矩阵(orthogonal matrix)
正交矩阵的逆矩阵
通俗公式
如果选择绕x、y、z轴旋转,即分别取 n =(1,0,0)、n =(0,1,0)、n =(0,0,1),那么对应的旋转矩阵是
案例
仿射变换
齐次坐标
- (x,y,z,0)表示向量
- (x,y,z,1)表示点
仿射变换定义
- 仿射变换:一个线性变换加上一个平移变量 b,即
α(u)=τ(u)+b
- 或者用矩阵表示法,A是一个线性变换的矩阵表示
- 如果w=1把坐标扩充为齐次坐标,那么把上述公式简化成
平移
恒等变换
恒等变换是一种直接返回其输入参数的线性变换,如 I(u)=u
如果将平移变换定义为仿射变换,贼其中的线性变换就是一种恒等变换,即
平移矩阵
平移矩阵的逆矩阵
案例
缩放和旋转的放射矩阵
- 如果放射变换的 b=0 ,则放射变换就是线性变换
- 意味着,可以通过一个 4 x 4 的放射矩阵表达任意线性变换
4x4的缩放矩阵
4x4的旋转矩阵
仿射变换矩阵的几何意义
变换的复合
假设S为缩放矩阵,T为平移矩阵,R为旋转矩阵
因为矩阵乘法(不满足交换律),所以我么可以定义C=SRT,即把3个矩阵变成1个,方便计算
坐标变换
坐标变换矩阵/标架变换矩阵公式
结合律
- 3个标架F,G,H
- A:F转换到G的变换矩阵
- B:G转换到H的变换矩阵
- pF:F中的一个向量
- 求:此向量在标架H中的坐标PH
***(pF***A)B=PH
(pG)B=PH
- 由于矩阵乘法满足结合律,所以
pF(AB)=PH
逆矩阵
pFA=PH
PF=pHA-1
变换矩阵与坐标变换矩阵
在数学上,可以将改变几何体的变换解释为坐标变换,反之亦然
DirectXMath库的变换函数
1 | #pragma |
Markdown(Typora)数学公式
Ctrl+Shift+M激活公式
上下标
a^2+\ b_{y_3}
$$
a^2+\ b_{y_3}
$$
括号
- 中小括号
(a+b)*[c+2]
$$
(a+b)*[c+2]
$$
- 大括号需要加
\
\{1,2,3 \}
$$
{1,2,3 }
$$
- 尖括号
\langle x \rangle
$$
\langle x \rangle
$$
- 上下取整
\lceil x \rceil+\lfloor y\rfloor
$$
\lceil x \rceil+\lfloor y\rfloor
$$
- 特殊括号
\begin{pmatrix} 1\ 2\\ 3\ 4 \end{pmatrix}\\ \begin{bmatrix} 1\ 2\\ 3\ 4 \end{bmatrix}\\ \begin{Bmatrix} 1\ 2\\ 3\ 4 \end{Bmatrix}\\ \begin{vmatrix} 1& 2\\ 3& 4 \end{vmatrix}\\ \begin{Vmatrix} 1& 2\\ 3& 4 \end{Vmatrix}\\
$$
\begin{pmatrix} 1\ 2\ 3\ 4 \end{pmatrix}\
\begin{bmatrix} 1\ 2\ 3\ 4 \end{bmatrix}\
\begin{Bmatrix} 1\ 2\ 3\ 4 \end{Bmatrix}\
\begin{vmatrix} 1& 2\ 3& 4 \end{vmatrix}\
\begin{Vmatrix} 1 & 2\ 3 & 4 \end{Vmatrix}\
$$
希腊字母
1.\alpha\ A\\ 2.\beta\ B\\ 3.\gamma\ \Gamma\\ 4.\delta\ \Delta\\ 5.\epsilon\ E\\ 6.\zeta\ Z\\ 7.\eta\ H\\ 8.\theta\ \Theta\\ 9.\iota\ I\\ 10.\kappa\ K\\ 11.\lambda\ \Lambda\\ 12.\mu\ M\\ 13.\nu\ N\\ 14.\xi\ \Xi\\ 15.\omicron\ O\\ 16.\pi\ \Pi\\ 17.\rho\ P\\ 18.\sigma\ \Sigma\\ 19.\tau\ T\\ 20.\upsilon\ \Upsilon\\ 21.\phi\ \Phi\\ 22.\chi\ X\\ 23.\psi\ \Psi\\ 24.\omega\ \Omega\\
$$
1.\alpha\ A\
2.\beta\ B\
3.\gamma\ \Gamma\
4.\delta\ \Delta\
5.\epsilon\ E\
6.\zeta\ Z\
7.\eta\ H\
8.\theta\ \Theta\
9.\iota\ I\
10.\kappa\ K\
11.\lambda\ \Lambda\
12.\mu\ M\
13.\nu\ N\
14.\xi\ \Xi\
15.\omicron\ O\
16.\pi\ \Pi\
17.\rho\ P\
18.\sigma\ \Sigma\
19.\tau\ T\
20.\upsilon\ \Upsilon\
21.\phi\ \Phi\
22.\chi\ X\
23.\psi\ \Psi\
24.\omega\ \Omega\
$$
根式分式
\sqrt[x+y]{\frac ab}+\sqrt{c+2\over 50+x}
$$
\sqrt[x+y]{\frac ab}+\sqrt{c+2\over 50+x}
$$
字体
ABCabc+\ \mathbb{ ABCabc}+\ \Bbb{ ABCabc黑板粗体}\\ ABCabc+\mathbf{ABCabc黑体}\\ ABCabc+\mathtt{ABCabc打印字体} \\ ABCabc+\mathrm{ABCabc罗马字体} \\ ABCabc+\mathscr{ABCabc手写字体} \\ ABCabc+\mathfrak{ABCabc德国字体Fraktur}
$$
ABCabc+\ \mathbb{ ABCabc}+\ \Bbb{ ABCabc黑板粗体}\
ABCabc+\mathbf{ABCabc黑体}\
ABCabc+\mathtt{ABCabc打印字体} \
ABCabc+\mathrm{ABCabc罗马字体} \
ABCabc+\mathscr{ABCabc手写字体} \
ABCabc+\mathfrak{ABCabc德国字体Fraktur}
$$
表格
\begin{array}{c|lcr} n & \text{Left} & \text{Center} & \text{Right} \\ \hline 1 & 0.24 & 1 & 125 \\ 2 & -1 & 189 & -8 \\ 3 & -20 & 2000 & 1+10i \\ \end{array}
$$
\begin{array}{c|lcr} n & \text{Left} & \text{Center} & \text{Right} \ \hline 1 & 0.24 & 1 & 125 \ 2 & -1 & 189 & -8 \ 3 & -20 & 2000 & 1+10i \ \end{array}
$$
矩阵
\begin{matrix} 1 & x & x^2 \\ 1 & y & y^2 \\ 1 & z & z^2 \end{matrix}
$$
\begin{matrix} 1 & x & x^2 \ 1 & y & y^2 \ 1 & z & z^2 \end{matrix}
$$
向量等顶部符号
\vec{abc} \ ,\overline b\ ,\overrightarrow{cde} \ ,\dot c\ , \dot {adb}\ ,\ddot{acd}\ ,\dddot{adfe}
$$
\vec{abc} \ ,\overline b\ ,\overrightarrow{cde} \ ,\dot c\ , \dot {adb}\ ,\ddot{acd}\ ,\dddot{adfe}
$$
对其
需要使用&来指示需要对齐的位置
\begin{align} \sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \\ & = \sqrt{\frac{73^2}{12^2} \cdot \frac{73^2-1}{73^2}} \\ & = \frac{73}{12} \sqrt{1 - \frac{1}{73^2}} \\ & \approx \frac{73}{12} \left( 1 - \frac{1}{2 \cdot 73^2} \right) \end{align}
$$
\begin{align} \sqrt{37} & = \sqrt{\frac{73^2-1}{12^2}} \ & = \sqrt{\frac{73^2}{12^2} \cdot \frac{73^2-1}{73^2}} \ & = \frac{73}{12} \sqrt{1 - \frac{1}{73^2}} \ & \approx \frac{73}{12} \left( 1 - \frac{1}{2 \cdot 73^2} \right) \end{align}
$$
分类表达式
f(n) = \begin{cases} n/2, & \text{if $n$ is even} \\ 3n+1, & \text{if $n$ is odd} \end{cases}
$$
f(n) = \begin{cases} n/2, & \text{if $n$ is even} \ 3n+1, & \text{if $n$ is odd} \end{cases}
$$\left. \begin{array}{l} \text{if $n$ is even:} & n/2 \\ \text{if $n$ is odd:} & 3n+1 \end{array} \right\} = f(n)
$$
\left. \begin{array}{l} \text{if $n$ is even:} & n/2 \ \text{if $n$ is odd:} & 3n+1 \end{array} \right} = f(n)
$$
公式标记与引用
a:= x^2-y^3 \tag{公式1}\label{公式1}
$$
a:= x^2-y^3 \tag{公式1}\label{公式1}
$$a+y^3 \stackrel{\eqref{公式1}}=x^2
$$
a+y^3 \stackrel{\eqref{公式1}}=x^2
$$
其他
\to \rightarrow \leftarrow \Rightarrow \Leftarrow \mapsto
$$
\to \rightarrow \leftarrow \Rightarrow \Leftarrow \mapsto
$$\lt \gt \le \ge \neq
$$
\lt \gt \le \ge \neq
$$
\sin x\\ \arctan_y\\ \lim_{1\to\infty}\\
$$
\sin x\
\arctan_y\
\lim_{1\to\infty}\
$$\sum_1^n\ ,\int_1^{x+y}\
$$
\sum_1^n\ ,\int_1^{x+y}
$$
合集图片
DX学习笔记(二):DX初始化
Direct3D初始化
预备知识
组件对象模型
组件对象模型(Component Object Model,COM):不受DirectX语言束缚,并且向后兼容的技术
- 获得COM接口需要借助特定函数,而不是C++的new
- 删除COM有Release方法,而不是delete
Mirrosoft::WRL::ComPtr
类是Window是下的COM对象的智能指针- 当ComPtr出作用域时,它会自动调用Release方法
1 | //Get: 返回一个指向此底层COM接口的指针,此方法常用于把原始COM接口指针作为参数传给函数 |
1 | //GetAddressOf:返回指向此底层COM接口指针的地址,此函数可以利用函数参数返回COM接口的指针 |
1 | //Reset:将此ComPtr实例设置为nullptr释放与之相关的所有引用,同时减少COM接口引用次数,此方法功能与将ComPtr目标实例赋值nullptr效果相同 |
纹理格式
2D纹理是一种由数据元素构成的矩阵,每个元素存储的都是一个像素的颜色
DXGI_FORMAT_R32G32B32_FLOAT:每个元素由2个32位无符号整数分量构成,存储的不一定是颜色信息
DXGI_FORMAT_R16G16B16A16_UNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,1]**的区间
DXGI_FORMAT_R32G32_UINT:每个元素由2个32位无符号整数分量构成
DXGI_FORMAT_R8G8B8A8_UNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,1]**的区间
DXGI_FORMAT_R8G8B8A8_SNORM:每个元素由4个8位无符号分量构成,每个分量都被映射到**[-1,1]**的区间
DXGI_FORMAT_R9G9B9A9_SINT:每个元素由4个8位无符号分量构成,每个分量都被映射到**[-128,127]**的区间
DXGI_FORMAT_R8G8B8A8_UINT:每个元素由4个8位无符号分量构成,每个分量都被映射到**[0,255]**的区间
其他格式
- DXGI_FORMAT_R16G16B16A16_TYPELESS:每个元素由4个16位无符号分量构成,但是没有指出数据类型
交换链和页面翻转
- 前台缓冲区和后台缓冲区在绘制渲染过程中互换,这种操作称为:呈现(presenting,亦有译作提交、显示)
- 前后台缓冲区构成了交换链,Direct3D中用==
IDXGISwapChain
==接口来表示 - 这个接口不仅储存了前后台缓冲区的纹理,还提供了修改缓冲区大小(IDXGISwapChain::ResiezeBuffers)和呈现缓冲区内容(IDXGISwapChain::Present)的方法
- 使用2个缓冲区的情况称为双缓冲(double buffering,亦有译作双重缓冲、双倍缓冲等)
- 还可以用更多的缓冲区,使用3个缓冲区就叫作三重缓冲(triple buffering)
深度缓冲
深度缓冲区(depth buffer):存储的非图像数据,而是特定像素的深度信息
- 深度值范围 0.0-1.0
- 0.0代表观察者在视锥体(视域体、视景体、视截体、视体),即观察者能看到的空间范围
- 1.0代表观察者在视锥体中嫩通过看到的离自己最远的像素
- 如果后台缓冲区的分辨率位1280x1024,那么深度缓冲去也应当由1280x1024
深度缓冲区的原理:计算每个像素的深度值,并执行深度测试(depth test),具有最小深度值的像素会最终写入后台缓冲
深度缓冲区也是一种纹理,用如下格式来创建
- DXGI_FORMAT_D32_FLOAT_S8X24_UINT:占用64位,取其中的32位指定一个浮点型深度缓冲区,另有8位无符号整数分配给模板缓冲区(stencil buffer),并且将该元素映射到[0,255]
- DXGI_FORMAT_D32_FLOAT:指定一个32位浮点型深度缓冲区
- DXGI_FORMAT_D24_UNORM_指定一个无符号的24位深度缓冲区,并将该元素映射到[0,1]区间;另有8位无符号整数分配给模板缓冲区(stencil buffer),并且将该元素映射到[0,255]
- DXGI_FORMAT_D16_UNORM:指定一个16位浮点型深度缓冲区,并将该元素映射到[0,1]区间
资源与描述符
资源->中间层(即描述符)->GPU
描述符
- 一种把送往GPU的资源进行描述的轻量级结构;
- 绘制所需的资源通过描述符绑定到渲染流水线上;
- 为GPU解释资源,如告知Direct3D某个资源如何使用(绑定到流水线的哪个阶段);
- 指定欲绑定资源中的局部数据
常见描述符
- CBV/SRV/UAV :分表表示常量缓冲区(constant buffer view)、着色器资源视图(shader resource view)和无序访问试图(unordered access view)3种资源;
- ***采样器(sampler,亦有译作取样器)***:表示采样器资源(用于纹理)
- RTV:渲染目标视图资源(render target view)
- DSV:深度/模板视图资源(depth/stencil view)
描述符堆:存放某种特定类描述符的内存,可以看作是描述符数组
多重采样
超级采样(SSAA)
超级采样:反走样技术
- 使用4倍于屏幕分辨率大小的后台缓冲区和深度缓冲去;
- 3D场景以这种更大的分辨率渲染到后台缓冲区中;
- 当数据要从后台缓冲区调往屏幕显示的时候,会将后台缓冲区按4个像素一组进行解析(降采样,downsample),把放大的采样点数降低回原来采样点数每组用求平均值的方法得到相对平滑的像素颜色
- 实际上是通过软件的方式提升了画面分辨率
- 超级采样是高开销的操作,因为限速处理数量和占用内存大小都增加到了4倍,因此Direct3D支持一种性能和效果折中的反走样技术:多重采样(multisampling),记作MSAA
多重采样(MSAA)
- 多重采样不需要对每个子像素都进行计算
- 而是仅计算一次像素中心的颜色,在基于可视性和覆盖性将得到的颜色信息分享给其子像素
区别
超级采样:图像颜色要根据每一个像素来计算,因此每个子像素都可以各具不的颜色;开销更大但是更精确
多重采样:每个像素只需要计算一次,最后假尼姑得到的颜色数据复制到多边形覆盖的所有可见子像素中
用Direct3D进行多重采样
1 | typedef struct DXGI_SAMPLE_DESC |
根据给定的纹理格式和采样数量,用ID3D12Device::CheckFeatureSupport
方法查询对应的质量级别
- 考虑到多重采样会占用内存资源,又为了保证程序性能等原因,通常会把采样数量设定位 4 或 8
- 如果不希望使用多重采样,可以设置采样数量位1,质量设置为0
功能级别
Direct3D 11开始引用了功能级别(feature level),代码里用 D3D_FEATURE_LEVEL
表示
1 | enum D3D_FEATURE_LEVEL |
- 功能级别为不同级别所支持的功能进行严格界定
DirectX图形学基础结构
DirectX图形学基础结构(DXGI)是一种与Direct3D配合使用的API
如,IDXGIFactory
是DXGI中的关键接口之一,用于创建IDXGISwapChain
接口以及枚举显示适配器
一个系统可能由数个显示设备,我们称每一台显示设备都是一个显示输出,用IDXGIOutput
接口来表示
功能支持的检测
ID3D12Device::CheckFeatureSupport
方法是检测当前图形驱动对多重过采样的支持,圆形如下
1 | HRESULT ID3D12Device::CheckFeatureSupport(D3D12_FEATURE Feature,void* pFeatureSuportData,UINT FeatureSupportDataSize); |
***Feature:***枚举类型
D3D12_FEATURE
中的成员之一,用于指定我们希望检测的功能支持类型,具体如下- D2D12_FEATURE_D3D12_OPTIONS:检测当前图形驱动对Direct3D 12各种功能的支持情况
- D3D12_FEATURE_ARCHITECTURE:检测图形适配器中GPU的硬件体系架构特性
- D3D12_FEATURE_FEATURE_LEVELS:检测对功能级别的支持情况
- D3D12_FEATURE_FORMAT_SUPPORT:检测对给定纹理格式的支持情况
- D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:检测对多重采样功能的支持情况
pFeatureSuportData:指向某种数据结构的指针,该结构中存有检索到的特定功能支持的信息,此结构体的具体类型取决于Feature参数
- D3D12_FEATURE_D3D12_OPTIONS:返回一个D3D12_FEATURE_DATA_D3D12_OPTIONS实例
- D3D12_FEATURE_ARCHITECTURE:返回D3D12_FEATURE_ARCHITECTURE实例
- D3D12_FEATURE_FEATURE_LEVELS:同上类推
- D3D12_FEATURE_FORMAT_SUPPORT:同上类推
- D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS:同上类推
FeatureSupportDataSize:传回pFeatureSuportData参数中的数据结构大小
资源驻留
Direct3D12中,应用程序通过控制资源在显存中的去留,主动管理资源的驻留情况(Direct3D11中则有系统自动管理
)
一般情况,资源创建时就会驻留在显存中,被销毁时则清出。但是通过下面方法我们可以自己管理资源的驻留
1 | HRESULT ID3D12Device::MakeResident(UINT NumObjects,ID3D12Pageable* const *ppObjects); |
这两个方法的第二个参数都是ID3D12Pageable
资源数组,第一个参数表示该数组资源的数量
CPU和GPU的交互
- 每个GPU都至少维护这一个命令队列(command queue,本质上是环形缓冲区,即ring buffer)。
- 借助Direct3D API,CPU可以用命令列表(command list)将命令提交到这个队列中去
- 新加入的命令不会立即执行
- 假如命令列表空空如也,那么GPU会闲置
- 假如命令列表填满,那么CPU会在某个时刻保持空闲
在Direct3D 12中,命令队列被抽象为==ID3D12CommandQueue
==接口来表示,通过填写D3D12_COMMAND_QUEUE_DESC
结构体来表示队列,在通过调用ID3D12Device::CreateCommandQueue
方法来创建。
命令队列
创建命令队列
1 | //创建队列智能指针 |
内存分配器
内存分配器:存储命令列表里的命令,执行ID3D12CommandQueue::ExecuteCommandLists
方法时,命令队列就会引用分配器里的命令
1 | virtual HRESULT STDMETHODCALLTYPE CreateCommandAllocator( |
type
:命令列表类型D3D12_COMMAND_LIST_TYPE_DIRECT
:GPU可直接执行的命令D3D12_COMMAND_LIST_TYPE_BUNDLE
:打包的命令列表,一般不用
riid
:适配接口的COM IDppCommandAllocator
:输出指向所建命令分配器的指针
1 | //内存管理指针 |
创建命令列表
1 | //原型 |
- nodeMask:如果只有1个GPU,设置成0;多个GPU用于关联的物理GPU
- type:命令列表类型
- pCommandAllocator:所建命令列表关联的命令分配器,类型必须匹配
- pInitialState:指定命令列表的渲染流水线初始状态,一般可以设置为
nullptr
- riid:待创建的
ID3D12CommandList
接口的COM ID
- ppCommandList:输出指向所建命令列表的指针
小结
- 可以创建多个关联同一个命令分配器的命令列表
- 但是不能同时用他们记录命令
- 其中一个命令列表在记录命令时,必须关闭同一分配器的其他命令列表
- 要保证命令列表中的所有命令都会按顺序连续的添加到命令分配器内
- 当创建或重置一个命令列表时,它会处于“打开 ”状态,所以同时为同一命令列表分配器创建两个命令列表会报错
1 | ID3D12CommandQueue::ExecuteCommandList(C);//把命令添加到命令列表 |
GPU与CPU的同步
刷新命令队列:CPU会等待GPU完成所有命令处理,直到到达指定的***围栏点(fence point)***为止。
创建围栏
1 | HRESULT ID3D12Device::CreateFence(UINT64 InitialValue, |
1 | void D3DApp::FlushCommandQueue() |
资源转换
资源冒险:当GPU的写操作还没有完成或者还没有开始,却开始读取资源的情况
为了解决资源冒险问题,Direct3D设计了一组相关状态,如资源在创建的时候会初一默认状态,直到应用程序通过方法将其转换为另一种状态。
转换资源屏障(transition resource barrier):通过一个API调用来转换多个资源要用到的数组
初始化Direct3D
- 初始化流程
- 用
D3D12CreateDevice
函数创建ID3D12Device
接口实例 - 创建一个
ID3D12Fence
对象,并且查询描述符的大小 - 检测用户设备对
4X MSAA
质量级别的支持情况 - 一次创建命令队列、命令列表分配器和主命令列表
- 描述并创建交换链
- 创建应用程序所需的描述符堆
- 调整后台缓冲区的大小,并为它创建渲染图标视图
- 创建深度/模板缓冲区及与之关联的深度/模板视图
- 设置视口(viewport)和裁剪矩形(scissor rectangle)
创建设备
1 | HRESULT WINAPI D3D12CreateDevice( |
案例
1 | // Try to create hardware device. |
pAdapte
r:使用的显示适配器,如果空,则使用主显示适配器MinimumFeatureLevele
:应用程序需要硬件所支持的最低功能级别,如果适配器不支持此功能级别,则设备创建失败riid
:ID3D12Device
接口的COM IDppDevice
:返回创建的Direct3D 12设备
创建失败的话会尝试创建
WARP
设备
创建围栏
1 | ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, |
检测4X MSAA支持
1 | // Check 4X MSAA quality support for our back buffer format. |
创建命令队列和命令列表
命令队列:
ID3D12CommandQueue
命令分配器:
ID3D12CommandAllocator
命令列表:
ID3D12GraphicsCommandList
1 | //.h申明 |
创建交换链
DXGI_SWAP_CHAIN_DESC
结构体定义
1 | typedef struct DXGI_SWAP_CHAIN_DESC |
- BufferDesc:后台缓冲区的属性,主要是高度、宽度和像素格式
- SampleDesc:多重采样的质量级别以及对每个像素的采样次数
- BufferUsage:如果要将数据渲染到后台缓冲区,则设置为
DXGI_USAGE_RENDER_TARGET_OUTPUT
- BufferCount:缓冲区数量,指定为2即双缓冲
- OutputWindow:渲染窗口的句柄
- Windowed:true则窗口模式运行,否则全屏
- SwapEffect:指定为
DXGI_SWAP_EFFECT_FLIP_DISCARD
- Flags:可选;如果指定为
DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH
则程序切换为全屏时,将选择适用于当前窗口尺寸的显示模式;否则就采用当前桌面的显示模式
DXGI_MODE_DESC
1 | typedef struct DXGI_MODE_DESC |
执行创建交换链
1 | void D3DApp::CreateSwapChain() |
创建描述符堆
创建描述符堆来存储程序中要用到的描述符/视图,本例中需要创建两个描述符堆来存储SwapChainBufferCount
个RTV
,另外一个存储1个DSV
1 | //.h |
创建以后需要通过方法来获得描述符的句柄
1 | D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::CurrentBackBufferView()const |
1 | D3D12_CPU_DESCRIPTOR_HANDLE D3DApp::DepthStencilView()const |
创建渲染目标视图
资源不能与渲染流水线中的阶段直接绑定,所以必须先为资源创建视图(描述符),并将其绑定到流水线阶段
1 | virtual HRESULT STDMETHODCALLTYPE GetBuffer( |
- Buffer:后台缓冲区索引
- riid:COM ID
- ppSurface:返回
ID3D12Resource
接口的指针,即后台缓冲区
调用此方法后会增加计数,所以使用后需要释放,需通过ComPtr
然后获得后台缓冲区创建的渲染目标视图
1 | virtual void STDMETHODCALLTYPE CreateRenderTargetView( |
- pResource:指定用作渲染目标的资源
- pDesc:指向
D3D12_RENDER_TARGET_VIEW_DESC
数组结构体的指针 - DestDescriptor:引用所创建渲染目标视图的描述符句柄
1 | CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart()); |
创建深度/模板缓冲区及其视图
因为深度缓冲区就是一种2D纹理,所以我们通过填写D3D12_RESOURCE_DESC
结构体来描述纹理资源
再用ID3D12Device::CreateCommittedResource
方法来创建它
- D3D12_RESOURCE_DESC
1 | typedef struct D3D12_RESOURCE_DESC |
D3D12_RESOURCE_DIMENSION Dimension:资源的维度
- ```cpp
enum D3D12_RESOURCE_DIMENSION{ D3D12_RESOURCE_DIMENSION_UNKNOWN = 0, D3D12_RESOURCE_DIMENSION_BUFFER = 1, D3D12_RESOURCE_DIMENSION_TEXTURE1D = 2, D3D12_RESOURCE_DIMENSION_TEXTURE2D = 3, D3D12_RESOURCE_DIMENSION_TEXTURE3D = 4 } D3D12_RESOURCE_DIMENSION;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2. Width:像素单位的纹理宽度。对于缓冲区,此项是占用的字节数
3. Height:同上
4. DepthOrArraySize:纹素为单位的纹理深度,或者是纹理数组的大小
5. MipLevels:mipmap层级的数量
6. Format:DXGI_FORMAT枚举成员之一
7. SampleDesc:多重采样级别和每个像素的采样次数
8. Layout:D3D12_TEXTURE_LAYOUT枚举成员之一,用于指定纹理布局
9. Flags:杂项标记,对于深度/模板缓冲区资源,设置为`D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL`
> `GPU`资源都存储在`堆`中,本质是具有特定属性的`GPU`显存快
>
> `ID3D12Device::CreateCommitedResource`方法根据提供的属性创建一个资源和一个堆,把资源提交到这个堆
###### CreateCommittedResource
```cpp
virtual HRESULT STDMETHODCALLTYPE CreateCommittedResource(
_In_ const D3D12_HEAP_PROPERTIES *pHeapProperties,//资源提交到的堆的属性,见下
D3D12_HEAP_FLAGS HeapFlags,//额外标记,一般是D3D12_HEAP_FLAG_NONE
_In_ const D3D12_RESOURCE_DESC *pDesc,//描述待创建的资源
D3D12_RESOURCE_STATES InitialResourceState,//此参数来设置资源的初始状态
_In_opt_ const D3D12_CLEAR_VALUE *pOptimizedClearValue,//清楚资源的优化值,不需要就选择nullptr
REFIID riidResource,//COM ID
_COM_Outptr_opt_ void **ppvResource) = 0;//新创建的资源,指向ID3D12Resource的指针
- ```cpp
D3D12_HEAP_PROPERTIES
1 | typedef struct D3D12_HEAP_PROPERTIES |
D3D12_HEAP_TYPE
1 | enum D3D12_HEAP_TYPE |
示例
1 | // Create the depth/stencil buffer and view. |
1 | // 使用资源格式将描述符创建为整个资源的MIP级别0。 |
设置视口
1 | D3D12_VIEWPORT mScreenViewport; |
1 | typedef struct D3D12_VIEWPORT |
填好结构体以后通过函数ID3D12GraphicsCommandList::RSSetViewports
方法来设置视口
示例
1 | mScreenViewport.TopLeftX = 0; |
不能为同一个渲染目标指定多个视口
而多个视口则是一种用于对多个渲染目标同时进行渲染的高级技术
命令列表重置,视口也要重置
设置裁剪矩形
1 | typedef struct tagRECT |
1 | mScissorRect = { 0, 0, mClientWidth, mClientHeight }; |
不能为同一个渲染目标指定多个裁剪矩形。
多裁剪矩形是以各种用于同时对多个渲染目标进行渲染的高级技术
裁剪矩形需要随着命令列表重置而重置
计时与动画
性能计时器
QueryPerformanceCounter
函数来活得性能计时器测量的当前时刻值
1 | __int64 countsPerSec; |
通过如下方式转换为秒
1 | valueInSecs=valueInCounts * mSecondsPercount; |
调用2次
QueryPerformanceCounter
函数得到两个时间戳的相对插值
1 | __int64 A; |
B-A
即可获得执行期间的计数值,或者(B-1)*mSecondsPerCount
获得代码运行期间所花费的秒数
游戏计时器类
1 | class GameTimer |
帧与帧之间的时间间隔
1 | void GameTimer::Tick() |
次Tick
函数被调用在D3DApp::Run
函数
1 | int D3DApp::Run() |
Reset
方法初始化第一帧的数据,因为第一帧没有之前的帧
1 | void GameTimer::Reset() |
总时间
1 | float GameTimer::TotalTime()const |
应用程序框架示例
自己模仿框架的代码
DXApp.h
1 | #pragma once |
DXApp.cpp
1 | #include "DXApp.h" |