DX12 基础篇(下) 图形管线
前言
- 本篇是Hello World系列的下篇,主要讲解渲染管线和着色器
渲染管线
概述
-
什么是渲染管线
- 渲染管线是一个描述图形系统需要执行哪些步骤来将三维场景渲染成二维屏幕的概念模型,也就是把3D 模型转变成计算机显示的东西的过程
- 渲染管线是实时渲染的底层工具
-
渲染管线的主要功能
决定在给定虚拟相机、三维物体、光源、照明模式,以及纹理等诸多条件的情况下,生成或绘制一幅二维图像的过程
-
渲染管线概要
- 从概念上可以将管线分为三个阶段
-
应用程序阶段(The Application Stage)
-
应用阶段可分为以下三个任务
-
将数据加载到显存中
-
设置渲染状态
所谓渲染状态,即定义场景中的网格如何被渲染
-
调用Draw Call
draw call是一个命令,由cpu发起,gpu来接收,该命令仅仅指向一个需要被渲染的图元列表,不包括材质信息,也就是CPU来命令GPU进行渲染
-
-
主要任务
是在应用程序阶段的末端,将需要在屏幕上显示出来绘制的几何体(也就是绘制图元,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(三角形设定)
-
- 从概念上可以将管线分为三个阶段
-
GPU管线
GPU渲染管线位于应用阶段后,在给定draw call后,GPU会根据渲染状态和输入的顶点数据来进行计算
GPU管线位于渲染管线的后两个阶段,开发人员没有绝对的控制权。如下图所示
-
DX12 渲染管线
-
注意
由于操作所需的步骤取决于所使用的软件和硬件以及所需的显示特性,因此没有适用于
所有情况
的通用绘图管线
输入装配器阶段
-
输入装配器阶段(input-assembler (IA))的目的
- 从填充的缓冲区读取几何数据(顶点和索引),再将它们装配为几何图元,供其他流水线阶段使用
- 附加系统生成的值(附加在着色器输入或输出上的字符串,它传递有关参数预期用途的信息),来提高着色器效率
顶点着色器阶段
-
什么是顶点着色器?
- 顶点着色器是一个计算机程序,可以看作函数,兼顾
输入和输出
能力且输入输出的数据都为单个顶点 - 每个顶点着色器输入顶点可由多达32位4个组成部分的矢量组成,输出顶点可由多达16个32位4个组成部分的矢量组成
- 顶点着色器提供了修改/创建/忽略顶点相关属性的功能,这些属性包括颜色、法线、纹理坐标、位置
- 顶点着色器是一个计算机程序,可以看作函数,兼顾
-
为什么需要顶点着色器?
- 顶点着色器用于将每个3d空间中的顶点转换为2d坐标和深度值
-
顶点着色器阶段的目的
- 处理来自IA阶段的顶点,执行每个顶点需要的操作,如蒙皮、变形和照明等
- 必须完成的任务是将顶点从模型空间转换到齐次裁剪空间(透视除法无法在顶点着色器/几何着色器中进行)
-
顶点着色器示例
//顶点结构体 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阶段的两个系统生成值: VertexID 和 InstanceID
- 顶点着色器可以在不需要屏幕空间导数的情况下执行加载和纹理采样操作
- 若没有使用几何着色器,顶点着色器必须使用SV_POSITION语义输出顶点在齐次裁剪空间中的位置。因为这一情况下,硬件要获取这一位置
几何着色器阶段
-
什么是几何着色器?
- 它运行应用程序指定的着色器代码,以顶点作为输入(输入是
完整的图元
),并能在输出上生成顶点 - 它可以改变传递进来的图元拓扑结构,且能接收任何拓扑类型的图元
- 它运行应用程序指定的着色器代码,以顶点作为输入(输入是
-
为什么需要几何着色器?
可以修改网格,实现酷炫的效果
-
注意
- 几何着色器不能创建顶点,只能接受输入的单个顶点
- 主要优点是可以创建/销毁几何体
裁剪
-
什么是裁剪?
丢弃完全位于平截头体外的几何体
-
为什么需要裁剪?
无需渲染摄像机看不到的部分,提高性能
-
裁剪算法 Sutherland-Hodgeman clipping algorithm(苏泽兰-霍奇曼)
思路是找到平面与多边形的所有交点,并将这些顶点按顺序组织成新的裁剪多边形
光栅化阶段
-
什么是光栅化?
简单来说,光栅化就是把一个图元转变为一个二维图像的过程(每个图元被转换为像素)
-
光栅化的主要任务
为投影至屏幕上的3D三角形计算出对应的像素颜色
像素着色器
-
什么是像素着色器?
像素着色器又称片元着色器,主要作用是进行像素的处理,让复杂的着色方程在每个像素上执行(结合常量、纹理数据、每个顶点的插值值和其他数据来产生每个像素的输出)
-
为什么需要像素着色器?
它根据顶点的插值属性作为输入来计算对应的像素颜色,主要可以实现一些特效(如,光照、反射、阴影)
输出合并
-
什么是输出合并?
输出合并又称逐片元操作,具有
高度可配置性
的。像素着色器生成的像素片段会被送至输出合并阶段,此阶段中,一些像素片段可能被丢弃,剩下的像素片段被写入后台缓冲区 此阶段使用管道状态、由像素着色器生成的像素数据、渲染目标的内容、深度/模板缓冲区的内容的组合生成最终呈现的像素颜色
顾名思义,它进行合并操作,还分管颜色修改、z缓冲、混合、模板和相关缓存的处理
-
为什么需要输出合并?
输出合并阶段使用深度和模板值来确定是否应该绘制像素.而每个像素的颜色信息被存储在一个名为颜色 缓冲的地方。因此,当我们执行渲染时,颜色缓冲中往往已经有了上次渲染之后的颜色结果,那么,我们是使用这次渲染得到的颜色完全覆盖掉之前的结果,还是进行其他处理?这就是合并需要解决的问题
-
主要任务
- 决定每个片元的可见性。涉及许多测试工作,如深度测试、模板测试
- 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和
已经存储在颜色缓冲区中的颜色进行合并,或者说是混合
-
合并流程
编译着色器
-
为什么需要对着色器进行编译?
因为驱动需要获取着色器被编译后的一种可移植的字节码,并将其重新编译为针对当前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
它描述的是一段普通的内存块。可以用作数据缓冲区,在
网格优化
和加载操作期间存储顶点、邻接和材料信息
。此外,这些对象用于在编译顶点、几何图形和像素着色器的 API 中返回目标代码和错误消息
为什么需要ID3DBlob呢?
在GPU上,对于大部分资源的描述一般都是用地址起点加上对象内存容量实现的,如常量缓冲区.而这些资源一般十分庞大,一般不能直接上传,而是现在CPU端预处理为Blob,再上传至GPU供GPU使用
-
获得指向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; }
-
-
-
注意
- 仅仅对着色器编译不会使它和渲染流水线相绑定
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
DX12削笔机(小笔记)基础DX12API介绍(2) - 知乎 (zhihu.com)
GPU编程与CG语言之阳春白雪下里巴人
Unity Shader入门精要
Directx12 3D 游戏开发实战