前言
- 本篇是Hello World系列的下篇,主要讲解渲染管线和着色器
渲染管线
-
什么是渲染管线
- 渲染管线是一个描述图形系统需要执行哪些步骤来将三维场景渲染成二维屏幕的概念模型,也就是把3D 模型转变成计算机显示的东西的过程
- 渲染管线是实时渲染的底层工具
-
渲染管线的主要功能
决定在给定虚拟相机、三维物体、光源、照明模式,以及纹理等诸多条件的情况下,生成或绘制一幅二维图像的过程
-
渲染管线概要
- 从概念上可以将管线分为三个阶段
- 应用程序阶段(The Application Stage)
主要任务
是在应用程序阶段的末端,将需要在屏幕上显示出来绘制的几何体(也就是绘制图元,rendering primitives,如点、线、矩形等)输入到绘制管线的下一个阶段- 对于被渲染的每一帧,应用程序阶段将
摄像机位置,光照和模型的图元
输出到管线的下一个主要阶段 - 这个阶段是建立在软件的基础上实现的,开发者可以改变实现来改变实际性能。而其他阶段是全部或部分建立在硬件基础上,要改变实现是很困难的
- 应用程序阶段通常用于实现碰撞检测、加速算法、输入检测,动画,力反馈以及纹理动画,变换 仿真、几何变形,及一些不在其他阶段执行的计算,如层次视锥裁剪等加速算法
- 几何阶段(The Geometry Stage)
- 主要负责大部分
多边形
操作和顶点
操作,计算量很大 - 又可划分为如下几个阶段
- Model and View Transform(模型视图变换)
模型变换
的目的
是将模型变换到适合渲染的空间中。模型变换的对象
一般是模型的顶点和法线
视图变换
的目的是将摄像机放于坐标原点,以方便后续操作
- Vertex Shading(顶点着色)
- 着色:确定材质的光照效果。着色过程涉及对象上的各个点计算着色方程
- 一般来说,顶点着色在世界空间中进行
- 顶点着色计算完后,会将结果发送至光栅化阶段进行插值
- Projection(投影)
- 投影:将视体变换到一个对角顶点分别是(-1,-1,-1)和(1,1,1)单位立方体内,在完成显示后,z坐标不再保存在投影图片中
- 目的:将模型从三维空间投射至二维空间
- 两种投影方式
- 正交投影
- 透视投影
- Clipping(裁剪)
- 目的:对部分位于视体内部的图元进行裁剪
- 只有当图元完全或部分存在于视体内部时,才需要将其发送到光栅化阶段
- 图元相对物体内部的位置的处理情况
- 当图元完全位于视体内部,直接进行下一个阶段
- 当图元完全位于视体外部,不会进入下一个阶段,可直接丢弃,因为它们无需进行渲染
- 当图元部分位于视体内部,则需要对那些部分位于视体内的图元进行裁剪处理
- Screen Mapping(屏幕映射)
- 目的:将之前步骤得到的坐标映射到对应的屏幕坐标系
- 只有在视体内部经过裁剪的图元和完全位于视体内的图元,才可以进入到屏幕映射阶段。在这个阶段中,坐标仍然是三维(尽管投影后已成二维图片)
- Model and View Transform(模型视图变换)
- 主要负责大部分
- 光栅化阶段(The Rasterizer Stage)
- 光栅化:给定经过变换和投影之后的顶点,颜色以及纹理坐标(均来自于几何阶段),给每个像素正确配色,以便正确绘制整幅图像
- 可分为如下几个阶段
- Triangle Setup(三角形设定)
- 目的:计算三角形表面的差异和三角形表面的其他相关数据。该数据主要用于扫描转换和插值
- Triangle Traversal(三角形遍历/扫描转换)
- 目的:进行逐像素检查操作,检查该像素处的像素中心是否由三角形覆盖,而对于有三角形部分重合的像素,将在其重合部分生成片段
- Pixel Shading(像素着色)
- 目的:计算所有需逐像素操作的过程。使用插值得来的着色数据作为输入,输出结果为一种或多种将被传送到下一阶段的颜色信息
- 像素着色在可编程GPU中进行,这一段可以使用大量技术,如纹理贴图
- Merging(融合)
- 目的:合成当前储存于缓冲器中的由之前的像素着色阶段产生的片段颜色,以及处理可见性问题
- Triangle Setup(三角形设定)
- 应用程序阶段(The Application Stage)
- 从概念上可以将管线分为三个阶段
-
DX12 GPU渲染管线
-
注意
由于操作所需的步骤取决于所使用的软件和硬件以及所需的显示特性,因此没有适用于
所有情况
的通用绘图管线
输入装配器阶段
-
输入装配器阶段(input-assembler (IA))的目的
- 从填充的缓冲区读取几何数据(顶点和索引),再将它们装配为几何图元,供其他流水线阶段使用
- 附加系统生成的值(附加在着色器输入或输出上的字符串,它传递有关参数预期用途的信息),来提高着色器效率
-
图元拓扑
-
什么是图元拓扑?
告知D3D如何用顶点数据表示几何图元
-
定义和方法
-
表示图元拓扑
D3D_PRIMITIVE_TOPOLOGY
typedef enum D3D_PRIMITIVE_TOPOLOGY { D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0, //尚未用图元拓扑进行初始化 D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1, //点列表 D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2, //线列表 D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3, //线条带 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4, //三角形列表 D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5, //三角形带 D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10, //邻接的线列表 D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11, //邻接的线条带 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12, //邻接的三角形列表 D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13, //邻接的三角形带 //... }
-
点列表(D3D_PRIMITIVE_TOPOLOGY_POINTLIST)
点列表是独立的顶点的集合。可用于
星型字段
或在多边形表面上使用虚线
-
线列表(D3D_PRIMITIVE_TOPOLOGY_LINELIST)
线列表是独立的直线段列表,它的线段可以
彼此分开
。可用于渲染雨夹雪
或大雨
等 -
线条带(D3D_PRIMITIVE_TOPOLOGY_LINESTRIP)
线条带是连接的线段组成的基元,它的线段是
彼此相连
。使用线条带时,定点将在绘制调用的过程中被连接为一系列的连续线段。线条带用于创建未闭合的多边形
-
三角形列表(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST)
三角形列表是一系列孤立的三角形,它们可能在一起,也可能不在。可用于创建由不相交的部分组成的对象,如力场墙
-
三角形带(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP)
三角形带是一系列连接的三角形,不需要为每个三角形重复指定三个顶点。大多数物体都有三角形带组成
绘制顺序
:顺时针,因此需要将偶数序号的三角形带的后两个顶点顺序进行置换 -
具有邻接数据的图元拓扑
-
为什么需要邻接数据的图元拓扑?
因为几何着色器通常需要访问邻接三角形来实现特定的几何着色算法,如轮廓检测
-
例子
邻接的线列表,每个线列表都有两个与之相邻的线段
邻接的三角形列表,每个三角形都有三个与之相邻的三角形
-
注意
- 邻接信息仅在几何着色器中对D3D可见
-
-
-
为IA阶段绑定图元类型信息
void ID3D12GraphicsCommandList::IASetPrimitiveTopology( [in] D3D12_PRIMITIVE_TOPOLOGY PrimitiveTopology );
-
-
-
顶点缓冲区和索引缓冲区
-
为什么需要索引?
这是因为一个复杂的物体的不同三角形可能会共用许多顶点,随着模型复杂度的增加,复制顶点的数量会急剧上升。如此一来,会导致
增加内存的需求
和GPU的处理负担
三角形带可以改善这个问题,但它不够灵活,三角形列表显然更加灵活
因此,需要用索引移除每个三角形使用的重复顶点
-
目的
顶点缓冲区用于向IA阶段提供顶点数据;索引缓冲区用于向IA阶段提供索引,此缓冲区是可选的
-
-
堆
-
什么是堆?
其本质是有特定属性的GPU显存块
-
为什么需要堆?
用于存放GPU资源,因为CPU和GPU的内存块类型是不同的,CPU和GPU不可互相访问对方的内存块
-
三种常用的堆
typedef enum D3D12_HEAP_TYPE { D3D12_HEAP_TYPE_DEFAULT = 1, D3D12_HEAP_TYPE_UPLOAD = 2, D3D12_HEAP_TYPE_READBACK = 3, D3D12_HEAP_TYPE_CUSTOM = 4 } ;
-
D3D12_HEAP_TYPE_DEFAULT(默认堆)
默认堆为GPU提供
最大的带宽
(GPU访问这个堆速率是最快的),但CPU不能访问
-
D3D12_HEAP_TYPE_UPLOAD(上传堆)
上传堆CPU可以访问,此堆提交的都是需经CPU上传至GPU的资源.适合于CPU写一次,GPU读一次的数据
两种常用用途
- 使用来自 CPU 的数据初始化默认堆中的资源
- 将动态数据上传到每个顶点或像素重复读取的常数缓冲区中
-
D3D12_HEAP_TYPE_READBACK(回读堆)
回读堆可以从GPU读取数据,CPU可以访问,此堆提交的都是需要有CPU读取的资源
-
-
注意
纹理
不能是上传堆或回读堆- 尽量使用默认堆,这个性能最佳
上传堆
不要用于每帧重新初始化资源内容
和上传常量数据
- 自定义堆主要用于UMA 优化、多引擎、多适配器
-
-
系统生成值
-
什么是系统生成值(System-Generated Values)?
系统生成的值是由 IA 阶段生成
-
为什么需要系统生成值?
以便在着色器中提高效率
-
类别
-
VertexID
- 每个着色器阶段使用顶点 ID 来标识每个顶点,它是一个默认值为0的32位无符号整数
- 在 IA 阶段处理图元时,它被分配给一个顶点
- 将顶点id语义附加到着色器输入声明,以通知 IA 阶段为每个顶点生成顶点id
- 对于每个绘制调用(draw call),顶点 id 增加1.通过索引绘制调用,计数重置回开始值
- 对于 ID3D11DeviceContext: : DrawIndexed 和 ID3D11DeviceContext: : DrawIndexedInstances,顶点 id 表示索引值
- 若顶点 id 溢出(超过232-1) ,则其包装为0
-
PrimitiveID
- IA 阶段将向每个基元添加一个基元 ID,以供几何着色器或像素着色器阶段使用,它是一个默认值为0的32位无符号整数
- 在 IA 阶段处理图元时,它被分配给一个图元
- 将图元id语义附加到着色器输入声明,以通知 IA 阶段为每个图元生成图元id
- 对于每个索引绘制调用,图元 id 增加1,但是,每当新实例开始时,图元 id 重置为0
-
InstanceID
- 每个着色器阶段使用一个实例 id 来标识当前正在处理的几何图形的实例,它是一个默认值为0的32位无符号整数
- 若顶点着色器输入声明包含实例id语义,IA 阶段将向每个顶点添加一个实例id
- 对于每个索引绘制调用,实例 id 增加1。所有其他绘图调用都不更改实例 id 的值。如果实例 id 溢出(超过232-1) ,它将赋值为0
-
-
-
初始化IA
-
创建顶点结构体
创建一个结构体来容纳选定的顶点数据
struct Vertex1 { XMFLOAT3 Pos; XMFLOAT4 Color; }; struct Vertex2 { XMFLOAT3 Pos; XMFLOAT3 Normal; XMFLOAT2 Tex0; XMFLOAT2 Tex1; }
-
创建输入布局描述(input layout description)
描述IA阶段的输入缓冲区数据.输入布局描述用于向D3D提供顶点结构体的描述,让他知道如何处理结构体中的每个成员。如,D3D12_INPUT_LAYOUT_DESC数组为每个顶点元素指定与之相关的语义,顶点着色器的每个参数也附有一个语义,该语义用于使顶点元素和顶点着色器参数一一匹配
typedef struct D3D12_INPUT_LAYOUT_DESC { const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs; //描述顶点结构体中的成员 UINT NumElements; //上述数组中的元素个数 } D3D12_INPUT_LAYOUT_DESC;
-
D3D12_INPUT_ELEMENT_DESC数组中的元素描述顶点结构体中所对应的单个成员
typedef struct D3D12_INPUT_ELEMENT_DESC { LPCSTR SemanticName; UINT SemanticIndex; DXGI_FORMAT Format; UINT InputSlot; UINT AlignedByteOffset; D3D12_INPUT_CLASSIFICATION InputSlotClass; UINT InstanceDataStepRate; } D3D12_INPUT_ELEMENT_DESC;
-
SemanticName(语义)
它传达了元素的用途,通过语义即可将顶点结构体中的元素与顶点着色器输入签名中的元素一一映射
-
SemanticIndex
语义的索引。顶点结构体中相同语义的元素可能不只一个,用索引即可区分它们
-
Format
通过DXGI_FORMAT枚举中的成员来顶点结构体中元素的格式
-
InputSlot
指定传递元素所用的输入槽索引。D3D渲染管线有16个输入槽,可通过它们向IA阶段传递顶点数据
-
AlignedByteOffset
当前输入槽中,从某顶点结构体的首地址到某元素的起始地址的偏移量(字节表示)
-
InputSlotClass
标识单个输入槽包含的数据类型
typedef enum D3D12_INPUT_CLASSIFICATION { D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA = 0, //输入数据是每个顶点 D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA = 1 //输入数据是每个实例 } ;
-
InstanceDataStepRate
在缓冲区去往下一个元素前,使用相同的每个实例数据进行绘制的数量。对于顶点,此值必须为0
-
-
-
顶点缓冲区
-
创建顶点缓冲区描述
//创建缓冲区资源 CD3DX12_RESOURCE_DESC static inline Buffer(UINT64 width, //缓冲区所占字节数 D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE, UINT64 alignment = 0 )
-
创建顶点缓冲区。因为顶点缓冲区需要CPU进行写入且默认堆GPU读的速率是最快的,因此我们需要创建上传堆和默认堆,将资源复制到上传堆后,再从上传堆复制到默认堆
//创建一个资源和一个堆,并将资源提交到此堆 HRESULT CreateCommittedResource( [in] const D3D12_HEAP_PROPERTIES *pHeapProperties, //堆类型 [in] D3D12_HEAP_FLAGS HeapFlags, [in] const D3D12_RESOURCE_DESC *pDesc, //资源描述符 [in] D3D12_RESOURCE_STATES InitialResourceState, //资源初始化状态 [in, optional] const D3D12_CLEAR_VALUE *pOptimizedClearValue, //描述一个用于清除资源的优化值 [in] REFIID riidResource, [out, optional] void **ppvResource ); //描述堆属性 typedef struct D3D12_HEAP_PROPERTIES { D3D12_HEAP_TYPE Type; D3D12_CPU_PAGE_PROPERTY CPUPageProperty; D3D12_MEMORY_POOL MemoryPoolPreference; UINT CreationNodeMask; UINT VisibleNodeMask; } D3D12_HEAP_PROPERTIES; //一些堆属性的辅助函数 struct CD3DX12_HEAP_PROPERTIES : public D3D12_HEAP_PROPERTIES{ CD3DX12_HEAP_PROPERTIES(); explicit CD3DX12_HEAP_PROPERTIES(const D3D12_HEAP_PROPERTIES &o); CD3DX12_HEAP_PROPERTIES(D3D12_CPU_PAGE_PROPERTY cpuPageProperty, D3D12_MEMORY_POOL memoryPoolPreference, UINT creationNodeMask = 1, UINT nodeMask = 1); explicit CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE type, UINT creationNodeMask = 1, UINT nodeMask = 1); operator const D3D12_HEAP_PROPERTIES&() const; bool inline operator==( const D3D12_HEAP_PROPERTIES& l, const D3D12_HEAP_PROPERTIES& r ); bool inline operator!=( const D3D12_HEAP_PROPERTIES& l, const D3D12_HEAP_PROPERTIES& r ); };
//子资源结构体 typedef struct D3D12_SUBRESOURCE_DATA { const void *pData; //指向包含子资源数据的内存块 LONG_PTR RowPitch; //子资源数据的行间距(以字节为单位) LONG_PTR SlicePitch; //子资源数据的深度间距(以字节为单位) } D3D12_SUBRESOURCE_DATA;
//完成创建上传堆和默认堆,将缓冲区资源复制到上传堆后,再从上传堆复制到默认堆的工作 Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer( ID3D12Device* device, ID3D12GraphicsCommandList* cmdList, const void* initData, //要上传的数据的地址 UINT64 byteSize, //上传数据的总大小 Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer) { ComPtr<ID3D12Resource> defaultBuffer; //创建默认堆 ThrowIfFailed(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteSize), D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(defaultBuffer.GetAddressOf()))); //创建上传堆 ThrowIfFailed(device->CreateCommittedResource( &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD), D3D12_HEAP_FLAG_NONE, &CD3DX12_RESOURCE_DESC::Buffer(byteSize), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(uploadBuffer.GetAddressOf()))); //描述要复制到默认堆的数据 //子资源也就是资源的子集 D3D12_SUBRESOURCE_DATA subResourceData = {}; subResourceData.pData = initData; subResourceData.RowPitch = byteSize; subResourceData.SlicePitch = subResourceData.RowPitch; //将数据复制到默认堆 //UpdateSubresources()先将数据从CPU端的内存中复制到上传堆 //再把上传堆中的数据复制到默认堆 cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST)); UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData); cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ)); //不能立即对上传堆进行销毁,因为命令列表的复制操作可能尚未执行 return defaultBuffer; }
-
将顶点缓冲区绑定至IA
-
创建顶点缓冲区描述符
无需为顶点缓冲区描述符创建描述符堆
//顶点缓冲区描述符结构体 typedef struct D3D12_VERTEX_BUFFER_VIEW { D3D12_GPU_VIRTUAL_ADDRESS BufferLocation; //待创建视图的顶点缓冲区资源在GPU中的虚拟地址 UINT SizeInBytes; //缓冲区的大小(以字节为单位) UINT StrideInBytes; //每个顶点元素占用的字节数 } D3D12_VERTEX_BUFFER_VIEW;
-
为IA阶段设置顶点缓冲区描述符。将描述符和管线的输入槽绑定
void ID3D12GraphicsCommandList::IASetVertexBuffers( [in] UINT StartSlot, //当绑定多个顶点缓冲区时,所用的起始输入槽 [in] UINT NumViews, //输入槽绑定的顶点缓冲区数量,pViews数组中的元素个数 [in, optional] const D3D12_VERTEX_BUFFER_VIEW *pViews //顶点缓冲区视图数组 );
-
绘制图元
void ID3D12GraphicsCommandList::DrawInstanced( [in] UINT VertexCountPerInstance, //每个实例要绘制的顶点数 [in] UINT InstanceCount, //要绘制的实例个数.暂设为1 [in] UINT StartVertexLocation, //第一个被绘制的顶点的索引 [in] UINT StartInstanceLocation //从顶点缓冲区读取每个实例数据前添加每个索引的值。暂设为0 );
-
-
-
索引缓冲区
-
创建索引缓冲区描述符
无需为索引缓冲区描述符创建描述符堆
typedef struct D3D12_INDEX_BUFFER_VIEW { D3D12_GPU_VIRTUAL_ADDRESS BufferLocation; //索引缓冲区的GPU虚拟地址 UINT SizeInBytes; //索引缓冲区的大小(以字节为单位) DXGI_FORMAT Format; //必须为DXGI_FORMAT_R16_UINT或DXGI_FORMAT_R32_UINT } D3D12_INDEX_BUFFER_VIEW;
-
将索引缓冲区描述符绑定至IA
void ID3D12GraphicsCommandList::IASetIndexBuffer( [in, optional] const D3D12_INDEX_BUFFER_VIEW *pView );
-
绘制图元
void ID3D12GraphicsCommandList::DrawIndexedInstanced( [in] UINT IndexCountPerInstance, //每个实例要绘制的索引数 [in] UINT InstanceCount, //要绘制的实例数。暂设为1 [in] UINT StartIndexLocation, //索引缓冲区想读取的起始索引位置 [in] INT BaseVertexLocation, //从顶点缓冲区读取顶点前,为每个索引加上此值 [in] UINT StartInstanceLocation //在从顶点缓冲区读取每个实例数据之前添加到每个索引的值。暂设为0 );
BaseVertexLocation主要用于将多个物体的顶点和索引连接为全局顶点缓冲区和全局索引缓冲区。因为合并后各自局部的索引缓冲区内的索引需要重新计算,计算方式是原索引+前一个几何体的顶点地址
-
-
-
注意
- 若着色器不使用顶点缓冲区和索引缓冲区,则无需创建和绑定缓冲区
顶点着色器阶段
-
什么是顶点着色器?
- 顶点着色器是一个计算机程序,可以看作函数,兼顾
输入和输出
能力且输入输出的数据都为单个顶点 - 每个顶点着色器输入顶点可由多达32位4个组成部分的矢量组成,输出顶点可由多达16个32位4个组成部分的矢量组成
- 顶点着色器提供了修改/创建/忽略顶点相关属性的功能,这些属性包括颜色、法线、纹理坐标、位置
- 顶点着色器是一个计算机程序,可以看作函数,兼顾
-
为什么需要顶点着色器?
- 顶点着色器用于将每个3d空间中的顶点转换为2d坐标和深度值
-
顶点着色器阶段的目的
- 处理来自IA阶段的顶点,执行每个顶点需要的操作,如蒙皮、变形和照明等
- 必须完成的任务是将顶点从模型空间转换到齐次裁剪空间(透视除法无法在顶点着色器/几何着色器中进行)
-
从模型空间转换到齐次裁剪空间
-
流程
具体的请看这里从零手写一个软渲染器day3 MVP变换 - 爱莉希雅 - 博客园 (cnblogs.com)
-
局部(模型)空间
- 为什么需要局部空间?
- 易于使用。物体中心通常位于局部空间的原点,且关于主轴对称
- 物体可以跨越多个场景而重复使用
- 多次绘制同一个物体,但这些物体的位置、方向、大小各不相同
- 为什么需要局部空间?
-
世界空间
- 局部空间到世界空间的转换
(LARGE W = S * R * T, S为缩放矩阵,R为旋转矩阵,T为*移矩阵 \ LARGE 相对于世界空间的齐次坐标的原点、xyz轴分别为Q_w = (Q_x, Q_y, Q_z, 1),u_w = (u_x, u_y, u_z, 0),v_w = (v_x, v_y, v_z), w_w = (w_x, w_y, w_z) \ LARGE W = left[begin{matrix} u_x & u_y & u_z & 0 \ v_x & v_y & v_z & 0 \ w_x & w_y & w_z & 0 \ Q_x & Q_y & Q_z & 1 end{matrix}right])
- 局部空间到世界空间的转换
-
观察空间
-
从世界空间到观察空间的转换
-
从观察空间到世界空间
(LARGE 观察空间中相对于世界空间的齐次坐标的原点、xyz轴分别为Q_w = (Q_x, Q_y, Q_z, 1),u_w = (u_x, u_y, u_z, 0),v_w = (v_x, v_y, v_z), w_w = (w_x, w_y, w_z) \ LARGE W = left[begin{matrix} u_x & u_y & u_z & 0 \ v_x & v_y & v_z & 0 \ w_x & w_y & w_z & 0 \ Q_x & Q_y & Q_z & 1 end{matrix}right])
世界坐标系和观察坐标系通常只有位置和朝向的差异,因此可以直接表示为(W = RT)
-
从世界空间到观察空间
我们只需对上述矩阵求逆,又因为旋转矩阵是正交矩阵,只需对旋转矩阵求转置即可(减少计算量)
(LARGE V = W^{-1} = (RT)^{-1} = T^{-1}R^{T} = W = left[begin{matrix} 1 & 0 & 0 & 0 \ 0 & 1 & 0 & 0 \ 0 & 0 & 1 & 0 \ -Q_x & -Q_y & -Q_z & 1 end{matrix}right] left[begin{matrix} u_x & v_x & w_x & 0 \ u_y & v_y & w_y & 0 \ u_z & v_z & w_z & 0 \ 0 & 0 & 0 & 1 end{matrix}right] = left[begin{matrix} u_x & v_x & w_x & 0 \ u_y & v_y & w_y & 0 \ u_z & v_z & w_z & 0 \ -Q·u & -Q·v & -Q·w & 1 end{matrix}right])
-
构建观察矩阵的直观方法
设Q为摄像机位置,T为摄像机的观察目标点,j为世界空间向上方向的单位向量(一般定为(0,1,0),因为世界观察空间向上向量方向相同),有这三个条件即可构建观察矩阵
//DirectXMath提供的计算观察矩阵的函数 XMMATRIX XM_CALLCONV XMMatrixLookAtLH( FXMVECTOR EyePosition, //摄像机位置Q FXMVECTOR FocusPosition, //观察目标点T FXMVECTOR UpDirection //世界空间向上方向的单位向量j )
-
-
-
齐次裁剪空间
-
*截头体:摄像机可观察到的空间体积,用一个四棱台表示
-
齐次裁剪空间的目的:将*截头体内的3D几何体投影到一个2D投影窗口中
-
从观察空间到齐次裁剪空间
-
定义*截头体
设(**面n、远*面f、垂直视场角(fov)alpha,宽高比r),以这四个参数定义一个
以原点作为投影的中心,并沿z轴正方向进行观察
的*截头体-
投影窗口
投影窗口本质上是观察空间中场景的2D图像(r = w / h,其中w为投影窗口宽度,h为投影窗口高度)。因为投影窗口的图像最终会被映射至后台缓冲区,因此应将投影窗口和后台缓冲区的纵横比保持一致
注意,投影窗口的大小并不重要,重点在于宽高比,处于方便,将h设为2
-
水*视场角(hov)(beta)
给定(垂直视场角alpha,宽高比r),即可求得水*视场角(beta)
求证过程如下(LARGE r = frac{w}{h} Rightarrow w = 2r \ LARGE 设投影窗口到原点的距离为d,可得d:tan(frac{alpha}{2}) = frac{1}{d} Rightarrow d = cot(frac{alpha}{2}) \ LARGE 可得水*视场角beta: tan(frac{beta}{2}) = frac{r}{d} = frac{r}{cot(frac{alpha}{2})} = r · tan(frac{alpha}{2}) Rightarrow LARGE beta = 2arctan(r · tan(frac{alpha}{2})))
-
-
投影窗口的任一一点
利用相似三角形求得给定点((x, y, z))在投影窗口z = d的投影((x', y', d))
求证如下:
(LARGE frac{x'}{d} = frac{x}{z} Rightarrow x' = frac{xd}{z} = frac{xcot(alpha / 2)}{z} = frac{x}{ztan(alpha/2)} \ LARGE frac{y'}{d} = frac{y}{z} Rightarrow y' = frac{yd}{z} = frac{ycot(alpha/2)}{z} = frac{y}{ztan(alpha/2)} \ LARGE 若点(x,y,z)位于*截头体内,则以下应成立 LARGE x' in [-r, r],y' in [-1,1], z in[n,f])
-
规格化设备坐标
在这之前,我们设定投影窗口高2,宽2r,但这会产生一个问题——因为硬件会涉及与投影窗口大小有关的操作,所以我们需要将宽高比告诉硬件。如果能去除这一动作,那过程会更加简便
解决之道:将x坐标上的投影区间[-r, r]缩放至规范化区间[-1,1](规格化设备坐标,NDC)
推导如下
(LARGE x' in [-r, r] Rightarrow frac{x'}{r} in [-1,1] \ LARGE 即可求得,x' = frac{x}{rz tan(alpha / 2)}, y' = frac{y}{z tan(alpha/2)})
-
以矩阵表示投影公式
可以看到上述式子(x',y')为非线性的,不能用矩阵表示。因此,我们需要将其分为线性和非线性两部分来处理。非线性部分除以z,但这之后我们还需要对z进行归一化深度,而这一操作进行后便无z可用。因此我们需要保存z,可以通过齐次坐标将z坐标保存到w中
齐次除法推导如下
非线性部分:
(LARGE P = left[begin{matrix} frac{1}{r tan(alpha/2)} & 0 & 0 & 0 \ 0 & frac{1}{tan(alpha/2)} & 0 & 0 \ 0 & 0 & A & 1 \ 0 & 0 & B & 0 end{matrix}right])
线性部分:(LARGE (x,y,z,1))
上面两个部分相乘,再除以w = z(除以0的点会被裁剪掉)
(LARGE left[begin{matrix} frac{x}{r tan(frac{alpha}{2})} & frac{y}{tan(frac{alpha}{2})} & Az + B & zend{matrix}right] stackrel{除以w}Rightarrow left[begin{matrix} frac{x}{rz tan(frac{alpha}{2})} & frac{y}{z tan(frac{alpha}{2})} & A + frac{B}{z} & 1 end{matrix}right])
-
归一化深度值
目的:将z坐标从[n,f]映射至[0,1],且规范化处理后深度关系不变
推导如下
(LARGE g(z) = A + frac{B}{z} \ 将**面映射为0,远*面映射为1:g(n) = A + frac{B}{n} = 0,g(f) = A + frac{B}{f} = 1 \ 解得:B = -An。 A + frac{-An}{f} = 1 Rightarrow A = frac{f}{f-n} \ 即,g(z) = frac{f}{f-n} - frac{nf}{(f-n)z})
此函数图像如下,可以看到此函数是非线性的,这会造成精度问题,因此建议令
*远*面尽可能接*
-
透视投影矩阵
(LARGE P = left[begin{matrix} frac{1}{r tan(alpha/2)} & 0 & 0 & 0 \ 0 & frac{1}{tan(alpha/2)} & 0 & 0 \ 0 & 0 & frac{f}{f-n} & 1 \ 0 & 0 & - frac{nf}{(f-n)} & 0 end{matrix}right])
DirectXMath库有专门的函数来构建透视投影矩阵
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH( float FovAngleY, //垂直视场角 float Aspect, //宽高比 float NearZ, //**面距离 float Farz //远*面距离 )
-
-
-
-
-
顶点着色器示例
//顶点结构体 struct Vertex { XMFLOAT3 Pos; XMFLOAT4 Color; } //为每个顶点元素指定与之相关的语义,让着色器和顶点结构体一一匹配 D3D12_INPUT_ELEMENT_DESC vertexDesc[] = { {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}, {"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0} }; //常量缓冲区(这种属于HLSL语法) cbuffer cbPerObject : register(b0) { float4x4 gWorldViewProj; } //顶点着色器 void VS(float3 iPosL : POSITION, //POSITION为语义 float4 iColor : COLOR, //COLOR为语义 out float4 oPosH : SV_POSITION, out float4 oCOlor : COLOR) { oPosH = mul(float4(iPosL, 1,0f), gWorldViewProj); //将颜色传入流水线下一阶段 oColor = iColor; }
上例中,VS即为顶点着色器他完成了从模型空间到齐次裁剪空间的任务
SV_POSITION中SV代表系统值,它修饰的顶点着色器输出元素存有齐次裁剪空间中的顶点位置信息,使得在进行裁剪、深度测试、光栅化等处理时,实现其他属性无法介入的有关运算 -
输入布局描述符和输入签名
- 输入布局中的元素格式定义的是 在数据未进入着色器寄存器前,应使用何种数据转换来确定各元素的具体格式;而着色器输入签名定义的是 在不修改输入寄存器中所存数据的情况下,顶点着色器应如何解释这些数据类型
- IA阶段的顶点属性和输入布局描述的定义相关,若顶点数据与VS期望的输入不相符,会导致错误
- 顶点数据无需和VS的输入签名完全匹配,但必须向VS提供输入签名所定义的顶点数据
- 若顶点结构体和输入签名有匹配的顶点元素,但数据格式不同,这也是合法的。因为DX允许用户对输入寄存器中数据的类型重新加以解释
-
注意
- 顶点着色器阶段可以使用来自IA阶段的两个系统生成值: VertexID 和 InstanceID
- 顶点着色器可以在不需要屏幕空间导数的情况下执行加载和纹理采样操作
- 若没有使用几何着色器,顶点着色器必须使用SV_POSITION语义输出顶点在齐次裁剪空间中的位置。因为这一情况下,硬件要获取这一位置
编译着色器
-
为什么需要对着色器进行编译?
因为驱动需要获取着色器被编译后的一种可移植的字节码,并将其重新编译为针对当前GPU所
优化的本地指令
-
方法
-
为给定的目标编译HLSL代码到字节码中
//d3dcompiler.h HRESULT D3DCompileFromFile( [in] LPCWSTR pFileName, //希望编译的以.hlsl作为扩展名的HLSL源代码文件 [in, optional] const D3D_SHADER_MACRO *pDefines, //D3D_SHADER_MACRO结构体的数组 [in, optional] ID3DInclude *pInclude, //指向编译器用于处理include文件的ID3Dinclude接口。若将此参数设为NULL且着色器包含#include,则会编译错误 [in] LPCSTR pEntrypoint, //着色器的入口点函数名。.hlsl文件可能存在多个着色器 [in] LPCSTR pTarget, //指定所用着色器类型和版本的字符串 [in] UINT Flags1, //指示对着色器代码应如何编译 [in] UINT Flags2, //指定编译器如何编译effect [out] ID3DBlob **ppCode, //指向ID3DBlob接口的指针。该接口存储了编译好的着色器字节码 [out, optional] ID3DBlob **ppErrorMsgs //指向ID3DBlob接口的指针。若编译中发生错误,会存储报错的字符串 );
-
ID3DInclude
用户实现的一个include接口,允许app调用用户可重写的方法来打开和关闭着色器include文件
若要使用此接口,需创建一个从ID3Dinclude继承的接口,并给方法实现自定义行为
//用户实现方法,用于关闭着色器#include文件 HRESULT ID3DInclude::Close( LPCVOID pData //指向包含include指令的缓冲区 ); //用户实现方法,用于打开和读取着色器#include文件的内容 HRESULT ID3DInclude::Open( D3D_INCLUDE_TYPE IncludeType, //指示#include文件的位置 LPCSTR pFileName, //#include文件的名称 LPCVOID pParentData, //指向包含#include文件的容器 LPCVOID *ppData, //指向包含#include指令的缓冲区.此指针在调用 ID3Dinclude::Close()前一直有效 UINT *pBytes //指向Open()在ppData中返回的字节数 ); //D3D_INCLUDE_TYPE typedef enum _D3D_INCLUDE_TYPE { D3D_INCLUDE_LOCAL = 0, D3D_INCLUDE_SYSTEM, D3D10_INCLUDE_LOCAL, D3D10_INCLUDE_SYSTEM, D3D_INCLUDE_FORCE_DWORD = 0x7fffffff } D3D_INCLUDE_TYPE;
-
pTarget
Direct3D feature levels - Win32 apps | Microsoft Learn不同版本支持的着色器版本
-
ID3DBlob
此接口用于返回任意长度的数据。它描述的是一段普通的内存块
-
获得指向ID3DBlob对象中数据的void*类型的指针
LPVOID ID3D10Blob::GetBufferPointer();
-
返回对象中的数据大小
SIZE_T ID3D10Blob::GetBufferSize();
-
-
辅助函数
运行时
编译着色器ComPtr<ID3DBlob> d3dUtil::CompileShader( const std::wstring& filename, const D3D_SHADER_MACRO* defines, const std::string& entrypoint, const std::string& target) { UINT compileFlags = 0; #if defined(DEBUG) || defined(_DEBUG) compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION; #endif HRESULT hr = S_OK; ComPtr<ID3DBlob> byteCode = nullptr; ComPtr<ID3DBlob> errors; hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE, entrypoint.c_str(), target.c_str(), compileFlags, 0, &byteCode, &errors); if(errors != nullptr) OutputDebugStringA((char*)errors->GetBufferPointer()); ThrowIfFailed(hr); return byteCode; }
-
-
-
注意
- 仅仅对着色器编译不会使它和渲染流水线相绑定
曲面细分阶段
-
什么是曲面细分(tessellation)?
它利用在硬件中实现镶嵌化处理技术对网格中的三角形进行细分,来增加物体表面的三角形数量
-
为什么需要曲面细分?
管线评估求值更低的多边形计数,但可以呈现更高的细节
-
曲面细分的优点
- 使离摄像机较*的三角形经镶嵌化处理得到更丰富的细节,而对摄像机较远的三角形不进行任何改动。以这种方式,可以只针对用户关注度高的部分网格增添三角形,提高其细节
- 在内存中仅维护简单的低精度模型网格,再根据需求为他动态增添额外的三角形,来节省资源
- 在处理动画和物理模拟时采用低模网格,在渲染时采用镶嵌化处理的高模网格
-
注意
- D3D11前,需要在CPU上实现曲面细分操作,再上传至GPU中;而这之后,便可直接在GPU上完成曲面细分
几何着色器阶段
-
什么是几何着色器?
- 它运行应用程序指定的着色器代码,以顶点作为输入(输入是
完整的图元
),并能在输出上生成顶点 - 它可以改变传递进来的图元拓扑结构,且能接收任何拓扑类型的图元
- 它运行应用程序指定的着色器代码,以顶点作为输入(输入是
-
为什么需要几何着色器?
可以修改网格,实现酷炫的效果
-
注意
- 几何着色器不能创建顶点,只能接受输入的单个顶点
- 主要优点是可以创建/销毁几何体
裁剪
-
什么是裁剪?
丢弃完全位于*截头体外的几何体
-
为什么需要裁剪?
无需渲染摄像机看不到的部分,提高性能
-
裁剪算法 Sutherland-Hodgeman clipping algorithm(苏泽兰-霍奇曼)
思路是找到*面与多边形的所有交点,并将这些顶点按顺序组织成新的裁剪多边形
光栅化阶段
-
什么是光栅化?
简单来说,光栅化就是把一个图元转变为一个二维图像的过程(每个图元被转换为像素)
-
光栅化的主要任务
为投影至屏幕上的3D三角形计算出对应的像素颜色
像素着色器
-
什么是像素着色器?
像素着色器又称片元着色器,主要作用是进行像素的处理,让复杂的着色方程在每个像素上执行(结合常量、纹理数据、每个顶点的插值值和其他数据来产生每个像素的输出)
-
为什么需要像素着色器?
它根据顶点的插值属性作为输入来计算对应的像素颜色,主要可以实现一些特效(如,光照、反射、阴影)
输出合并
-
什么是输出合并?
像素着色器生成的像素片段会被送至输出合并阶段,此阶段中,一些像素片段可能被丢弃,剩下的像素片段被写入后台缓冲区
此阶段使用管道状态、由像素着色器生成的像素数据、渲染目标的内容、深度/模板缓冲区的内容的组合生成最终呈现的像素颜色
顾名思义,它进行合并操作,还分管颜色修改、z缓冲、混合、模板和相关缓存的处理
-
为什么需要输出合并?
输出合并阶段使用深度和模板值来确定是否应该绘制像素
reference
[Game-Programmer-Study-Notes/README.md at master · QianMo/Game-Programmer-Study-Notes (github.com)](https://github.com/QianMo/Game-Programmer-Study-Notes/blob/master/Content/《Real-Time Rendering 3rd》读书笔记/README.md)
Common version interfaces - Win32 apps | Microsoft Learn
Direct3D feature levels - Win32 apps | Microsoft Learn
Direct3D 12 programming guide - Win32 apps | Microsoft Learn
Directx12 3D 游戏开发实战
文章来源: 博客园
- 还没有人评论,欢迎说说您的想法!