DirectX11学习笔记一 渲染一个三角形

*  这两个月我看了看数据可视化,又看了看OpenCV,草稿箱里的东西阅存越多,最后还是决定入坑计算机图形学了。因为本科的时候学过一点OpenGLES和计算机图形学原理,再加上自己对游戏开发感兴趣,所以不打算再继续跟老师的方向走了。本来选现在的老师是打算学学游戏服务器的,奈何失算了,o(︶︿︶)o 唉。
  本学习笔记的原始素材来自龙书DirectX11,全部代码和部分文字来自GitHub及其整理龙书代码的大佬的博客。
  本博文记录的是我自己的思维方式和理解方法,还有部分内容来自其他博客,MSDN,StackOverflow,视频网站等,比他们的更详细,尽量不一笔带过(因为我比较笨o(︶︿︶)o )。
本文主要使用DirectX11.1,Win10,VS2019,代码里各个类之间的调用关系比较简单,就不一一对应了。

一.Direct3D概述

  Direct3D是一种底层绘图API(application programming interface,应用程序接口),它可以让我们可以通过3D硬件加速绘制3D世界。从本质上讲,Direct3D提供的是一组软件接口,我们可以通过这组接口来控制绘图硬件。例如,要命令绘图设备清空渲染目标(例如屏幕),我们可以调用Direct3D的ID3D11DeviceContext::ClearRenderTargetView方法来完成这一工作。Direct3D层位于应用程序和绘图硬件之间,这样我们就不必担心3D硬件的实现细节,只要设备支持Direct3D 11,我们就可以通过Direct3D 11 API来控制3D硬件了。
  支持Direct3D 11的设备必须支持Direct3D 11规定的整个功能集合以及少数的额外附加功能(有一些功能,比如多重采样数量,仍然需要以查询方式实现,这是因为不同的Direct3D硬件这个值可能并不一样)。在Direct3D 9中,设备可以只支持Direct3D 9的部分功能;所以,当一个Direct3D 9应用程序要使用某一特性时,应用程序就必须先检查硬件是否支持该特性。如果要调用的是一个不为硬件支持Direct3D函数,那应用程序就会出错。而在Direct3D 11中,不需要再做这种设备功能检查,因为Direct3D 11强制要求设备实现Direct3D 11规定的所有功能特性。
  组件对象模型(COM)技术使DirectX独立于任何编程语言,并具有版本向后兼容的特性。我们经常把COM对象称为接口,并把它当成一个普通的C++类来使用。当使用C++编写DirectX程序时,许多COM的底层细节都不必考虑。唯一需要知道的一件事情是,我们必须通过特定的函数或其他的COM接口方法来获取指向COM接口的指针,而不能用C++的new关键字来创建COM接口。另外,当我们不再使用某个接口时,必须调用它的Release方法来释放它(所有的COM接口都继承于IUnknown接口,而Release方法是IUnknown接口的成员),而不能用delete语句——COM对象在其自身内部实现所有的内存管理工作。
  当然,有关COM的细节还有很多,但是在实际工作中只需知道上述内容就足以有效地使用DirectX了。
  注意:COM接口都以大写字母“I”为前缀。例如,表示2D纹理的接口为ID3D11Texture2D。

二.D3D初始化

  D3D初始化前还要初始化Window窗口程序的一些属性,因为不属于D3D,略过了,有兴趣的可以直接看代码,都是API的东西。
  D3D的初始化其实也是完全代码上的东西,可以先不了解图形渲染管线的知识,就当是扫盲和入门了。
  从bool D3DApp::InitDirect3D()方法出发,需要以下几个步骤:

    1. 创建D3D设备和设备上下文
    1. 创建交换链
    1. 设置DXGI交换链与Direct3D设备的交互

1.创建D3D设备和设备上下文

  什么是D3D设备和设备上下文?
  它们是是最重要的Direct3D接口,可以被看成是物理图形设备硬件的软控制器;也就是说,我们可以通过该接口与硬件进行交互,命令硬件完成一些工作(比如:在显存中分配资源、清空后台缓冲区、将资源绑定到各种管线阶段、绘制几何体)。具体而言:
  ID3D11Device接口用于检测显示适配器功能和分配资源。
  ID3D11DeviceContext接口用于设置管线状态、将资源绑定到图形管线和生成渲染命令。
  本文的设备上下文是ID3D11DeviceContext* md3dImmediateContext;,叫做立即执行上下文。
  还有种上下文叫延迟执行上下文(ID3D11Device::CreateDeferredContext)。该上下文主要用于DirectX11支持的多线程程序。
  创建D3D设备和设备上下文的方法:

HRESULT WINAPI D3D11CreateDevice(
    IDXGIAdapter* pAdapter,         // 1. [In_Opt]适配器
    D3D_DRIVER_TYPE DriverType,     // 2. [In]驱动类型
    HMODULE Software,               // 3. [In_Opt]若上面为D3D_DRIVER_TYPE_SOFTWARE则这里需要提供程序模块
    UINT Flags,                     // 4. [In]使用D3D11_CREATE_DEVICE_FLAG枚举类型
    D3D_FEATURE_LEVEL* pFeatureLevels,  // 5. [In_Opt]若为nullptr则为默认特性等级,否则需要提供特性等级数组
    UINT FeatureLevels,             // 6. [In]特性等级数组的元素数目
    UINT SDKVersion,                // 7. [In]SDK版本,默认D3D11_SDK_VERSION
    ID3D11Device** ppDevice,        // [Out_Opt]输出D3D设备
    D3D_FEATURE_LEVEL* pFeatureLevel,   // [Out_Opt]输出当前应用D3D特性等级
    ID3D11DeviceContext** ppImmediateContext ); //[Out_Opt]输出D3D设备上下文

pAdapter(适配器)

  我们可以将它看做是对显示卡设备的一层封装,通过该参数,我们可以指定需要使用哪个显示卡设备。通常该参数我们设为nullptr,这样就可以交由上层驱动来帮我们决定使用哪个显卡,或者在NVIDIA控制面板来设置当前程序要使用哪个显卡。如果想要在应用层决定,使用IDXGIFactory::EnumAdapters方法可以枚举当前可用的显示卡设备。

DriverType 驱动类型

D3D_DRIVER_TYPE原型:

typedef enum D3D_DRIVER_TYPE {
  D3D_DRIVER_TYPE_UNKNOWN,
  D3D_DRIVER_TYPE_HARDWARE,
  D3D_DRIVER_TYPE_REFERENCE,
  D3D_DRIVER_TYPE_NULL,
  D3D_DRIVER_TYPE_SOFTWARE,
  D3D_DRIVER_TYPE_WARP
} ;
驱动类型 描述
D3D_DRIVER_TYPE_UNKNOWN 表示未知驱动类型
D3D_DRIVER_TYPE_HARDWARE 一种可以应用D3D特性的硬件驱动。这是你D3D程序中应该主要使用的驱动,因为它效率最好。硬件驱动可以在支持的设备上使用硬件加速,也同样可以在设备中不支持的渲染管线部分使用软件加速。硬件驱动经常是硬件设备的一种抽象。
D3D_DRIVER_TYPE_REFERENCE 一种由软件驱动的支持所有D3D特性的引用驱动。引用驱动的目的是提高精度而不是速度。只用来作特性测试,或者debug,只是为了开发和测试来使用。
D3D_DRIVER_TYPE_NULL 空驱动,代表不支持渲染的引用驱动。主要用来debug非渲染API调用。这个驱动只有DirectX SDK有。
D3D_DRIVER_TYPE_SOFTWARE 软件驱动,纯粹用软件应用D3D特性。不适合高性能场景,因为它很慢。是硬件加速失效的替代品。
D3D_DRIVER_TYPE_WARP 高性能软件渲染器。

  一般都用D3D_DRIVER_TYPE_HARDWARE,但是我们要先检查一下当前系统环境到底支持哪一个,顺序从硬件-WARP-软件驱动来轮询,依次尝试能否成功。

④Flags 枚举值

Flags对应的是D3D11_CREATE_DEVICE_FLAG枚举值,如果需要D3D设备调试的话(在Debug模式下),可以指定D3D11_CREATE_DEVICE_DEBUG枚举值。指定该值后,可以在出现程序异常的时候观察调试输出窗口的信息。

⑤⑥pFeatureLevels 特征等级

D3D_FEATURE_LEVEL本身是一个枚举类型,里面是各种D3D的版本。只需要在D3D11CreateDevice方法里传入一个包含你想支持的等级的数组就可以了,⑥是数组的格元素个数。
代码

HRESULT hr = S_OK;

	// 创建D3D设备 和 D3D设备上下文
	UINT createDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)  
	createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
	// 驱动类型数组
	D3D_DRIVER_TYPE driverTypes[] =
	{
		D3D_DRIVER_TYPE_HARDWARE,
		D3D_DRIVER_TYPE_WARP,
		D3D_DRIVER_TYPE_REFERENCE,
	};
	UINT numDriverTypes = ARRAYSIZE(driverTypes);

	// 特性等级数组
	D3D_FEATURE_LEVEL featureLevels[] =
	{
		D3D_FEATURE_LEVEL_11_1,
		D3D_FEATURE_LEVEL_11_0,
	};
	UINT numFeatureLevels = ARRAYSIZE(featureLevels);

	D3D_FEATURE_LEVEL featureLevel;
	D3D_DRIVER_TYPE d3dDriverType;
	for (UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++)
	{
		d3dDriverType = driverTypes[driverTypeIndex];
		hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, featureLevels, numFeatureLevels,
			D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());

		if (hr == E_INVALIDARG)
		{
			// Direct3D 11.0 的API不承认D3D_FEATURE_LEVEL_11_1,所以我们需要尝试特性等级11.0以及以下的版本
			hr = D3D11CreateDevice(nullptr, d3dDriverType, nullptr, createDeviceFlags, &featureLevels[1], numFeatureLevels - 1,
				D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), &featureLevel, m_pd3dImmediateContext.GetAddressOf());
		}

		if (SUCCEEDED(hr))
			break;
	}

	if (FAILED(hr))
	{
		MessageBox(0, L"D3D11CreateDevice Failed.", 0, 0);
		return false;
	}

	// 检测是否支持特性等级11.0或11.1
	if (featureLevel != D3D_FEATURE_LEVEL_11_0 && featureLevel != D3D_FEATURE_LEVEL_11_1)
	{
		MessageBox(0, L"Direct3D Feature Level 11 unsupported.", 0, 0);
		return false;
	}

2. 创建交换链

  什么是交换链?
当模型的图元经过层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值。但是,为了避免我们看到那些正在进行光栅化的图元,GPU会使用双重缓冲(Double Buffering)的策略。这意味着,对场景的渲染时在幕后发生的,即在后置缓冲(Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区和前置缓冲区(Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。图4.1说明了这一过程。
DirectX11学习笔记一 渲染一个三角形_第1张图片
  我们首先渲染缓冲区B,它是当前的后台缓冲区。一旦帧渲染完成,前后缓冲区的指针会相互交换,缓冲区B会变为前台缓冲区,而缓冲区A会变为新的后台缓冲区。之后,我们将在缓冲区A中进行下一帧的渲染。一旦帧渲染完成,前后缓冲区的指针会再次进行交换,缓冲区A会变为前台缓冲区,而缓冲区B会再次变为后台缓冲区。
  前后缓冲区形成了一个交换链(swap chain)。在Direct3D中,交换链由IDXGISwapChain接口表示。该接口保存了前后缓冲区纹理,并提供了用于调整缓冲区尺寸的方法(IDXGISwapChain::ResizeBuffers)和呈现方法(IDXGISwapChain::Present)。我们会在4.4节中详细讨论些方法。
  使用(前后)两个缓冲区称为双缓冲(double buffering)。缓冲区的数量可多于两个;比如,当使用三个缓冲区时称为三缓冲(triple buffering)。不过,两个缓冲区已经足够用了。
  注意:虽然后台缓冲区是一个纹理(纹理元素称为texel),但是我们更习惯于将纹理元素称为像素(pixel),因为后台缓冲区存储的是颜色信息。有时,即使纹理中存储的不是颜色信息,人们还是会将纹理元素称为像素(例如,“法线贴图像素”)。
  但是创建交换链还要做一些准备工作。

①描述交换链

  要创建交换链,首先需要填充一个DXGI_SWAP_CHAIN_DESC1结构体来描述我们将要创建的交换链的特性。该结构体的定义如下。

typedef struct DXGI_SWAP_CHAIN_DESC1
{
    UINT Width;                     // 缓冲区宽度
    UINT Height;                    // 缓冲区高度
    DXGI_FORMAT Format;             // 缓冲区数据格式
    BOOL Stereo;                    // 忽略   
    DXGI_SAMPLE_DESC SampleDesc;    // 采样描述
    DXGI_USAGE BufferUsage;         // 缓冲区用途
    UINT BufferCount;               // 缓冲区数目
    DXGI_SCALING Scaling;           // 忽略
    DXGI_SWAP_EFFECT SwapEffect;    // 交换效果
    DXGI_ALPHA_MODE AlphaMode;      // 忽略
    UINT Flags;                     // 使用DXGI_SWAP_CHAIN_FLAG枚举类型
} DXGI_SWAP_CHAIN_DESC1;

typedef struct DXGI_SAMPLE_DESC
{
    UINT Count;                     // MSAA采样数
    UINT Quality;                   // MSAA质量等级
} DXGI_SAMPLE_DESC;

typedef struct DXGI_SWAP_CHAIN_FULLSCREEN_DESC
{
    DXGI_RATIONAL RefreshRate;                  // 刷新率
    DXGI_MODE_SCANLINE_ORDER ScanlineOrdering;  // 忽略
    DXGI_MODE_SCALING Scaling;                  // 忽略
    BOOL Windowed;                              // 是否窗口化
} DXGI_SWAP_CHAIN_FULLSCREEN_DESC;

typedef struct DXGI_RATIONAL
{
    UINT Numerator;                 // 刷新率分子
    UINT Denominator;               // 刷新率分母
} DXGI_RATIONAL;

②得到创建交换链的句柄

  本文使用DirectX11.1创建交换链的方法:

IDXGIFactory2::CreateSwapChainForHwnd( 
            _In_  IUnknown *pDevice, // 1.D3D设备
            _In_  HWND hWnd,  // 2.窗体程序句柄
            _In_  const DXGI_SWAP_CHAIN_DESC1 *pDesc, // 3.传入交换链描述
            _In_opt_  const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, // 4.全屏运行用的交换链描述
            _In_opt_  IDXGIOutput *pRestrictToOutput, // 5.传空指针就行
            _COM_Outptr_  IDXGISwapChain1 **ppSwapChain // 接收创建的交换链

  显然易见,我们得先得到IDXGIFactory2的对象,然后再调用这个方法。
  之前在创建D3D设备时使用的是默认的显卡适配器IDXGIAdapter(对于双显卡的笔记本大概率使用的是集成显卡),而创建出来的D3D设备本身实现了IDXGIDevice接口,通过该对象,我们可以获取到当前所用的显卡适配器IDXGIAdapter对象,这样我们再通过查询它的父级找到是哪个IDXGIFactory枚举出来的适配器。
  具体什么意思呢?就是先得到D3D对象保存的适配器,然后通过HR(dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast(dxgiFactory1.GetAddressOf())));得到IDXGIFactory2对象。
  那么IDXGIFactory2,IDXGIAdapter,IDXGIDevice之间到底是什么关系呢???GetParent意义是什么???
  先看一下StackOverflow上关于创建交换链的一些解释。

  这段代码是为了用DX11或之后的版本的接口创建交换链,同时这是特别为确保你使用的DXGI factor实例就是你创建D3D设备所使用的factory实例而开发的。
  基本上,当你第一次创建D3D11设备时,你可以选择提供一个IDXGIAdapter适配器对象来使用。大多数都用一个空指针让D3D设备自动指定一个默认的适配器了。为了完成交换链的初始化,然而,你必须得有一个DXGI factory对象。理论上你可以用DXGICreateFactory1来创建一个,但是你很容易搞砸而调用了传入错误枚举值的DXGICreateFactoryDXGICreateFactory2
  相反,最安全的方法是从你的ID3D11Device中得到IDXGIDevice。使用标准COM组件的IUnknown::QueryInterface:

IDXGIDevice * dxgiDevice = 0;
HRESULT hr = mD3dDevice->QueryInterface( __uuidof( IDXGIDevice ),( void ** ) & dxgiDevice );
if ( SUCCEEDED(hr) )

  接着从IDXGIDevice对象中使用IDXGIObject::GetParent得到IDXGIAdapter的适配器对象。

IDXGIAdapter * dxgiAdapter = 0;
hr = dxgiDevice->GetParent( __uuidof( IDXGIAdapter ),( void ** ) & dxgiAdapter );
if ( SUCCEEDED(hr) )

  然后再从IDXGIAdapter对象中再次使用IDXGIObject::GetParent

IDXGIFactory * dxgiFactory = 0;
hr = dxgiAdapter->GetParent( __uuidof( IDXGIFactory ),( void ** ) & dxgiFactory );
if ( SUCCEEDED(hr) )

  现在你就得到了与你的D3D设备关联的IDXGIFactory,不管你之前是怎么创建的D3D设备。一定记得COM引用计数器的意思是你现在得到了所有的这些引用还得释放掉。

dxgiFactory->Release();
dxgiAdapter->Release();
dxgiDevice->Release();

  注意IDXGIFactory::CreateSwapChain是DX11.0创建交换链的方法,而且D3D11CreateDeviceAndSwapChain方法的结果类似,而D3D11CreateDevice就不是。对于DX11.1或以后的版本,你最好用IDXGIFactory2::CreateSwapChainForHwnd,如果不是Win32桌面应用的话。对于Windows商店应用,Windows phone 8和XBOX One,你可以一直用IDXGIFactory2::CreateSwapChainForCoreWindow
  对于Win32桌面应用,你可以用以下的代码。

IDXGIFactory2* dxgiFactory2 = 0;
hr = dxgiFactory->QueryInterface( __uuidof(IDXGIFactory2), reinterpret_cast<void**>(&dxgiFactory2) );
if ( SUCCEEDED(hr) )
{
   // This system has DirectX 11.1 or later installed, so >we can use this interface
   dxgiFactory2->CreateSwapChainForHwnd( /* >parameters */ );
   dxgiFactory2->Release();
}
else
{
   // This system only has DirectX 11.0 installed
    dxgiFactory->CreateSwapChain( /* parameters */ >);
}

  See Anatomy of Direct3D 11 Create Device and the Direct3D tutorial sample Win32 desktop app version or Windows store app version.
  关于getParent的意义
  这是为了简化API。DXGIObject::GetParent永远能返回一个IUnknown或者一个DXGIObject对象,你得用QueryInterface来从中获取任何有用的信息。因此,GetParent只是取得了你想要的通用唯一识别码,所以他跟QueryInterface差不多,而且类型安全。

  至少稍微解释了一下创建的方法,但是还是没解决我们的问题。还是得查MSDN。
  Ⅰ.IDXGIFactory2 interface
  该接口包含创建比IDXGISwapChain功能更多的新版交换链的方法 and to monitor stereoscopic 3D capabilities.

方法 描述
IDXGIFactory2::CreateSwapChainForComposition Creates a swap chain that you can use to send Direct3D content into the DirectComposition API or the Windows.UI.Xaml framework to compose in a window.
IDXGIFactory2::CreateSwapChainForCoreWindow 创建与CoreWindow对象关联的交换链。
IDXGIFactory2::CreateSwapChainForHwnd 创建于HWND句柄关联的交换链。
IDXGIFactory2::GetSharedResourceAdapterLuid Identifies the adapter on which a shared resource object was created.
IDXGIFactory2::IsWindowedStereoEnabled Determines whether to use stereo mode.
IDXGIFactory2::RegisterOcclusionStatusEvent Registers to receive notification of changes in occlusion status by using event signaling.
IDXGIFactory2::RegisterOcclusionStatusWindow Registers an application window to receive notification messages of changes of occlusion status.
IDXGIFactory2::RegisterStereoStatusEvent Registers to receive notification of changes in stereo status by using event signaling.
IDXGIFactory2::RegisterStereoStatusWindow Registers an application window to receive notification messages of changes of stereo status.
IDXGIFactory2::UnregisterOcclusionStatus Unregisters a window or an event to stop it from receiving notification when occlusion status changes.
IDXGIFactory2::UnregisterStereoStatus Unregisters a window or an event to stop it from receiving notification when stereo status changes.
IDXGIFactory1::EnumAdapters1 Enumerates both adapters (video cards) with or without outputs.
IDXGIFactory1::IsCurrent Informs an application of the possible need to re-enumerate adapters.
IDXGIFactory::CreateSoftwareAdapter 创建代表软件适配器的适配器接口。
IDXGIFactory::CreateSwapChain 创建一个交换链。
IDXGIFactory::EnumAdapters Enumerates the adapters (video cards).
IDXGIFactory::GetWindowAssociation Get the window through which the user controls the transition to and from full screen.
IDXGIFactory::MakeWindowAssociation 允许DXGI监视应用的消息队列来判断用户是否可以用Alter-Enter开启关闭全屏。

  Ⅱ.IDXGIAdapter interface
  该接口代表了一个显示子系统(包含一个或多个GPU,DAC和显存)

方法 描述
IDXGIAdapter::CheckInterfaceSupport 检查系统是否支持图像组件的设备接口。
IDXGIAdapter::EnumOutputs 枚举适配器(显卡)输入
IDXGIAdapter::GetDesc 获取适配器(或显卡)的DXGI1.0描述。

  显示子系统通常代指显卡,但是,有些机器的显示子系统是主板的一部分。
  要枚举显示子系统,使用IDXGIFactory::EnumAdapters.
  要得到代表特定设备的适配器的接口,使用IDXGIDevice::GetAdapter.
  要创建软件适配器,调用IDXGIFactory::CreateSoftwareAdapter.
  Ⅲ.IDXGIDevice interface
  IDXGIDevice接口为生成图像数据的DXGI对象实现派生类。

方法 描述
IDXGIDevice::CreateSurface Returns a surface. This method is used internally and you should not call it directly in your application.
IDXGIDevice::GetAdapter 返回对应设备的适配器。
IDXGIDevice::GetGPUThreadPriority Gets the GPU thread priority.
IDXGIDevice::QueryResourceResidency Gets the residency status of an array of resources.
IDXGIDevice::SetGPUThreadPriority Sets the GPU thread priority.

  IDXGIDevice接口是设计用来给需要访问其他DXGI对象的DXGI对象们使用的。这个接口对于不使用Direct3D与DXGI交互的应用非常有用。
  Direct3D创建设备的方法会返回一个Direct3D设备对象。这个Direct3D设备对象实现了IUnknown接口。你可以查询Direct3D设备对象找到设备相关的IDXGIDevice接口。要找到Direct3D设备的IDXGIDevice接口,使用以下代码。

IDXGIDevice * pDXGIDevice;
hr = g_pd3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void **)&pDXGIDevice);

  看完了,还是一脸懵逼。似乎这一切都是围绕DXGI(DirectX Graphics Infrastructure)技术展开。

  简介: DirectX 图形基础架构,提供了对图形硬件进行底层管理的功能,与 D3D 的i图形功能独立。DXGI 可以说是提供了一个底层的通用框架用来支持未来的硬件。DXGI 的目的是沟通核心模式驱动和系统硬件。
  主要功能: 枚举显示硬件设备,将渲染好的帧呈现到输出设备,调整显示设备参数,全屏模式的切换等。

  还有个细节,创建D3D设备的方法只能得到最早版本的D3D设备,要想得到D3D设备1到5,需要先得到最低版本的,再通过它转换。
  详细的细节都后面需要了再回来看,现在知道的大概是怎么回事就差不多了,毕竟渲染才是重点。

3. 设置DXGI交换链与Direct3D设备的交互

  接下来的代码在OnResize()方法中。
  如果窗口的大小是固定的,则需要经历下面的步骤:

  1.获取交换链后备缓冲区的ID3D11Texture2D接口对象
  2.为后备缓冲区创建渲染目标视图ID3D11RenderTargetView
  3.通过D3D设备创建一个ID3D11Texture2D用作深度/模板缓冲区,要求与后备缓冲区等宽高
  4.创建深度/模板视图ID3D11DepthStrenilView,绑定刚才创建的2D纹理
  5.通过D3D设备上下文,在渲染管线的输出合并阶段设置渲染目标
  6.在渲染管线的光栅化阶段设置好渲染的视口区域
DirectX11学习笔记一 渲染一个三角形_第2张图片
步骤:
1.获取交换链的后备缓冲区
  后备缓冲区本质上是一个ID3D11Texture2D对象

A 2D texture interface manages texel data, which is structured memory.

  之前创建的交换链里设置的bufferCount= 1,所以已经有一个后备缓冲区了。通过IDXGISwapChain::GetBuffer方法直接获取后备缓冲区的ID3D11Texture2D接口

HRESULT IDXGISwapChain::GetBuffer( 
    UINT Buffer,        // [In]缓冲区索引号,从0到BufferCount - 1
    REFIID riid,        // [In]缓冲区的接口类型ID
    void **ppSurface);  // [Out]获取到的缓冲区

2.为后备缓冲区创建渲染目标视图
  资源不能被直接绑定到一个管现阶段;我们必须为资源创建资源视图,然后把资源视图绑定到不同的管线阶段。尤其是在把后台缓冲区绑定到管线的输出合并器阶段时(使Direct3D可以在后台缓冲区上执行渲染工作),我们必须为后台缓冲区创建一个渲染目标视图,ID3D11RenderTargetView interface。

A render-target-view interface identifies the render-target subresources that can be accessed during rendering.

  渲染目标视图(RenderTargetView)用于将渲染管线的运行结果输出给其绑定的资源,很明显它也只能够设置给输出合并阶段。渲染目标视图要求其绑定的资源是允许GPU读写的,因为在作为管线输出时会通过GPU写入数据,并且在以后进行混合操作时还需要再GPU读取该资源。通常渲染目标是一个二维的纹理,但它依旧可能会绑定其余类型的资源。这里不做讨论。
  现在我们需要将后备缓冲区绑定到渲染目标视图,使用ID3D11Devvice::CreateRenderTargetView方法来创建:

HRESULT ID3D11Device::CreateRenderTargetView( 
    ID3D11Resource *pResource,                      // [In]待绑定到渲染目标视图的资源
    const D3D11_RENDER_TARGET_VIEW_DESC *pDesc,     // [In]忽略
    ID3D11RenderTargetView **ppRTView);             // [Out]获取渲染目标视图

  现在这里演示了获取后备缓冲区纹理,并绑定到渲染目标视图的过程:

// 重设交换链并且重新创建渲染目标视图
	ComPtr<ID3D11Texture2D> backBuffer;
	HR(m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0));
	HR(m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf())));
	HR(m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf()));

3.创建深度/模版缓冲区
  ID3D11Device::CreateTexture2D – 创建一个2D纹理
  除了渲染目标视图外,我们还需要创建深度/模版缓冲区用于深度测试。深度/模版缓冲区也是一个2D纹理,要求其宽度和高度必须要和窗口宽高保持一致。
  通过D3D设备可以新建一个2D纹理,但在此之前我们必须先描述该缓冲区的信息。其本质上还是用描述信息创建一个2D纹理。

typedef struct D3D11_TEXTURE2D_DESC
{
    UINT Width;         // 缓冲区宽度
    UINT Height;        // 缓冲区高度
    UINT MipLevels;     // Mip等级
    UINT ArraySize;     // 纹理数组中的纹理数量,默认1
    DXGI_FORMAT Format; // 缓冲区数据格式
    DXGI_SAMPLE_DESC SampleDesc;    // MSAA采样描述
    D3D11_USAGE Usage;  // 数据的CPU/GPU访问权限
    UINT BindFlags;     // 使用D3D11_BIND_FLAG枚举来决定该数据的使用类型
    UINT CPUAccessFlags;    // 使用D3D11_CPU_ACCESS_FLAG枚举来决定CPU访问权限
    UINT MiscFlags;     // 使用D3D11_RESOURCE_MISC_FLAG枚举,这里默认0
}   D3D11_TEXTURE2D_DESC;  

  由于要填充的内容很多,并且目前只有在初始化环节才用到,因此这部分代码可以先粗略看一下,在后续的章节还会详细讲到。
填充好后,这时我们就可以用方法ID3D11Device::CreateTexture2D来创建2D纹理:

HRESULT ID3D11Device::CreateTexture2D( 
    const D3D11_TEXTURE2D_DESC *pDesc,          // [In] 2D纹理描述信息
    const D3D11_SUBRESOURCE_DATA *pInitialData, // [In] 用于初始化的资源
    ID3D11Texture2D **ppTexture2D);             // [Out] 获取到的2D纹理

  下面的代码是关于深度/模版缓冲区创建的完整过程

D3D11_TEXTURE2D_DESC depthStencilDesc;

depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.ArraySize = 1;
depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;

// 要使用 4X MSAA?
if (mEnable4xMsaa)
{
    depthStencilDesc.SampleDesc.Count = 4;
    depthStencilDesc.SampleDesc.Quality = m_4xMsaaQuality - 1;
}
else
{
    depthStencilDesc.SampleDesc.Count = 1;
    depthStencilDesc.SampleDesc.Quality = 0;
}

depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
depthStencilDesc.CPUAccessFlags = 0;
depthStencilDesc.MiscFlags = 0;

HR(m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()));

4.创建深度/模版视图
  有了深度/模版缓冲区后,就可以通过ID3D11Device::CreateDepthStencilView方法将创建好的2D纹理绑定到新建的深度/模版视图:ID3D11DepthStencilView

A depth-stencil-view interface accesses a texture resource during depth-stencil testing.

HRESULT ID3D11Device::CreateDepthStencilView( 
    ID3D11Resource *pResource,                      // [In] 需要绑定的资源
    const D3D11_DEPTH_STENCIL_VIEW_DESC *pDesc,     // [In] 深度缓冲区描述,这里忽略
    ID3D11DepthStencilView **ppDepthStencilView);   // [Out] 获取到的深度/模板视图

  演示如下:

HR(m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf()));

5.为渲染管线的输出合并阶段设置渲染目标
  ID3D11DeviceContext::OMSetRenderTargets方法要求同时提供渲染目标视图和深度/模板视图,不过这时我们都已经准备好了:

void ID3D11DeviceContext::OMSetRenderTargets( 
    UINT NumViews,                                      // [In] 视图数目
    ID3D11RenderTargetView *const *ppRenderTargetViews, // [In] 渲染目标视图数组
    ID3D11DepthStencilView *pDepthStencilView) = 0;     // [In] 深度/模板视图

  因此这里同样也是一句话的事情:

m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());
输出合并阶段的意义详情见下文渲染管线。

6.视口设置
  最终我们还需要决定将整个视图输出到窗口特定的范围。我们需要使用D3D11_VIEWPORT来设置视口。

Represents a viewport and provides convenience methods for creating viewports.

typedef struct D3D11_VIEWPORT
{
    FLOAT TopLeftX;     // 屏幕左上角起始位置X
    FLOAT TopLeftY;     // 屏幕左上角起始位置Y
    FLOAT Width;        // 宽度
    FLOAT Height;       // 高度
    FLOAT MinDepth;     // 最小深度,必须为0.0f
    FLOAT MaxDepth;     // 最大深度,必须为1.0f
}   D3D11_VIEWPORT;

ID3D11DeviceContext::RSSetViewports方法将设置1个或多个视口:

Bind an array of viewports to the rasterizer stage of the pipeline.

void ID3D11DeviceContext::RSSetViewports(
    UINT  NumViewports,                     // 视口数目
    const D3D11_VIEWPORT *pViewports);      // 视口数组

  将视图输出到整个屏幕需要按下面的方式进行填充:

m_ScreenViewport.TopLeftX = 0;
m_ScreenViewport.TopLeftY = 0;
m_ScreenViewport.Width    = static_cast<float>(mClientWidth);
m_ScreenViewport.Height   = static_cast<float>(mClientHeight);
m_ScreenViewport.MinDepth = 0.0f;
m_ScreenViewport.MaxDepth = 1.0f;

m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);

  完成了这六个步骤后,基本的初始化就完成了。但是,如果涉及到了窗口大小变化的情况,那么前面提到的后备缓冲区、深度/模版缓冲区、视口都需要重新调整大小。
  上面这六个步骤,简化的讲应该是三个步骤,初始化目标渲染视图,初始化深度/模版视图,将前面两者绑定到渲染管线,设置视口。
  注意,OnResize方法会在窗口大小变化时调用,获取到当前窗口的大小,然后重新设置上述6个小步骤,主要是调用IDXGISwapChain::ResizeBuffers。

Changes the swap chain’s back buffer size, format, and number of buffers. This should be called when the application window is resized.

(当然你在窗口大小变化时选择不去修改上述六个步骤好像也没什么问题(误))
  那么到现在为止,D3D的初始化(bool D3DApp::InitDirect3D())完成,接下来就是调用绘制方法,完成D3D渲染管线外围的设置。
简化的代码如下。

bool D3DApp::InitDirect3D()
{
	// ************************** 创建D3D设备 **************************
	D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_HARDWARE };  // 驱动类型数组
	D3D_FEATURE_LEVEL featureLevels[] = { D3D_FEATURE_LEVEL_11_1 }; // 特性等级数组
	D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_DEBUG, featureLevels, 1, //创建D3D设备
		D3D11_SDK_VERSION, m_pd3dDevice.GetAddressOf(), nullptr, m_pd3dImmediateContext.GetAddressOf());
	m_pd3dDevice.As(&m_pd3dDevice1);                      // 通过D3D设备获得D3D设备1
	m_pd3dImmediateContext.As(&m_pd3dImmediateContext1);  // 通过D3D设备上下文获得D3D设备上下文1

	m_pd3dDevice->CheckMultisampleQualityLevels(  	// 检测 MSAA支持的质量等级
		DXGI_FORMAT_R8G8B8A8_UNORM, 4, &m_4xMsaaQuality);
	// ************************** 创建D3D设备 **************************

	// ************************** 创建交换链 **************************
	ComPtr<IDXGIDevice> dxgiDevice = nullptr;
	ComPtr<IDXGIAdapter> dxgiAdapter = nullptr;
	ComPtr<IDXGIFactory2> dxgiFactory2 = nullptr;	// D3D11.1(包含DXGI1.2)特有的接口类
	m_pd3dDevice.As(&dxgiDevice);   //将D3D设备转换为IDXGI对象
	dxgiDevice->GetAdapter(dxgiAdapter.GetAddressOf());   // 获取适配器
	dxgiAdapter->GetParent(__uuidof(IDXGIFactory1), reinterpret_cast<void**>(dxgiFactory2.GetAddressOf())); // 获取工厂

	DXGI_SWAP_CHAIN_DESC1 sd;  	// 填充各种结构体用以描述交换链
	ZeroMemory(&sd, sizeof(sd));
	sd.Width = m_ClientWidth;
	sd.Height = m_ClientHeight;
	sd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
	sd.SampleDesc.Count = m_Enable4xMsaa?4:1;                   	// 是否开启4倍多重采样?
	sd.SampleDesc.Quality = m_Enable4xMsaa?m_4xMsaaQuality - 1:0;  	// 是否开启4倍多重采样?
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
	sd.BufferCount = 1;
	sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
	sd.Flags = 0;
	DXGI_SWAP_CHAIN_FULLSCREEN_DESC fd;
	fd.RefreshRate.Numerator = 60;
	fd.RefreshRate.Denominator = 1;
	fd.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	fd.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	fd.Windowed = TRUE;
	// 为当前窗口创建交换链
	dxgiFactory2->CreateSwapChainForHwnd(m_pd3dDevice.Get(), m_hMainWnd, &sd, &fd, nullptr, m_pSwapChain1.GetAddressOf());	
	m_pSwapChain1.As(&m_pSwapChain);
	// ************************** 创建交换链 **************************

	// 可以禁止alt+enter全屏
	dxgiFactory2->MakeWindowAssociation(m_hMainWnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES);

	// 每当窗口被重新调整大小的时候,都需要调用这个OnResize函数。现在调用
	// 以避免代码重复
	OnResize();

	return true;
}
void D3DApp::OnResize()
{
	// 释放渲染管线输出用到的相关资源
	m_pRenderTargetView.Reset();
	m_pDepthStencilView.Reset();
	m_pDepthStencilBuffer.Reset();

	// ************************** 创建渲染目标视图 **************************
	ComPtr<ID3D11Texture2D> backBuffer; 	// 重设交换链并且重新创建渲染目标视图
	m_pSwapChain->ResizeBuffers(1, m_ClientWidth, m_ClientHeight, DXGI_FORMAT_R8G8B8A8_UNORM, 0);
	m_pSwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), reinterpret_cast<void**>(backBuffer.GetAddressOf()));
	m_pd3dDevice->CreateRenderTargetView(backBuffer.Get(), nullptr, m_pRenderTargetView.GetAddressOf());
	// ************************** 创建渲染目标视图 **************************

	backBuffer.Reset();
	// ************************** 创建深度/模版视图 **************************
	D3D11_TEXTURE2D_DESC depthStencilDesc;
	depthStencilDesc.Width = m_ClientWidth;
	depthStencilDesc.Height = m_ClientHeight;
	depthStencilDesc.MipLevels = 1;
	depthStencilDesc.ArraySize = 1;
	depthStencilDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
	depthStencilDesc.SampleDesc.Count = m_Enable4xMsaa?4:1;                      // 要使用 4X MSAA? --需要给交换链设置MASS参数
	depthStencilDesc.SampleDesc.Quality = m_Enable4xMsaa?m_4xMsaaQuality - 1:0;  // 要使用 4X MSAA? --需要给交换链设置MASS参数
	depthStencilDesc.Usage = D3D11_USAGE_DEFAULT;
	depthStencilDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	depthStencilDesc.CPUAccessFlags = 0;
	depthStencilDesc.MiscFlags = 0;
	m_pd3dDevice->CreateTexture2D(&depthStencilDesc, nullptr, m_pDepthStencilBuffer.GetAddressOf()); // 创建深度缓冲区以及深度模板视图
	m_pd3dDevice->CreateDepthStencilView(m_pDepthStencilBuffer.Get(), nullptr, m_pDepthStencilView.GetAddressOf());
	// ************************** 创建深度/模版视图 **************************

	// 将渲染目标视图和深度/模板缓冲区结合到管线
	m_pd3dImmediateContext->OMSetRenderTargets(1, m_pRenderTargetView.GetAddressOf(), m_pDepthStencilView.Get());

	// 设置视口变换
	m_ScreenViewport.TopLeftX = 0;
	m_ScreenViewport.TopLeftY = 0;
	m_ScreenViewport.Width = static_cast<float>(m_ClientWidth);
	m_ScreenViewport.Height = static_cast<float>(m_ClientHeight);
	m_ScreenViewport.MinDepth = 0.0f;
	m_ScreenViewport.MaxDepth = 1.0f;
	m_pd3dImmediateContext->RSSetViewports(1, &m_ScreenViewport);
}

三.每帧画面的绘制

  代码比较简单,

void GameApp::DrawScene()
{
	assert(m_pd3dImmediateContext);
	assert(m_pSwapChain);
	static float blue[4] = { 0.0f, 0.0f, 1.0f, 1.0f };	// RGBA = (0,0,255,255)
	m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	HR(m_pSwapChain->Present(0, 0));
}

  大概就是三个部分

  • 1.清空需要绘制的缓冲区
    在每一帧画面绘制的操作中,我们需要清理一遍渲染目标视图绑定的缓冲区
void ID3D11DeviceContext::ClearRenderTargetView(
    ID3D11RenderTargetView *pRenderTargetView,  // [In]渲染目标视图
    const FLOAT  ColorRGBA[4]);                 // [In]指定覆盖颜色

  这里的颜色值范围都是0.0f到1.0f
  比如我们要对后备缓冲区(R8G8B8A8,RGBA都用8比特表示,多于8比特的设置在很多显示器上都用不到)使用蓝色进行清空,可以这么写:

float blue[4] = {0.0f, 0.0f, 1.0f, 1.0f};
m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), blue);
  • 2.清空深度/模版缓冲区
    同样在进行渲染之前,我们也要清理一遍深度/模版缓冲区
void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,  // [In]深度/模板视图
    UINT ClearFlags,    // [In]D3D11_CLEAR_FLAG枚举
    FLOAT Depth,        // [In]深度
    UINT8 Stencil);     // [In]模板初始值

  若要清空深度缓冲区,则需要指定D3D11_CLEAR_DEPTH,模版缓冲区则是D3D11_CLEAR_STENCIL
  每一次清空我们需要将深度值设为1.0f,模版值设为0.0f。其中深度值1.0f表示距离最远处:

m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
  • 3.前后台缓冲区交换并呈现
    完成一切绘制操作后就可以调用该方法了
HRESULT ID3D11DeviceContext::Present( 
    UINT SyncInterval,  // [In]通常为0
    UINT Flags);        // [In]通常为0

  如果此时执行代码,屏幕会是蓝色,但是我们需要绘制我们想要绘制的顶点,这就需要学习渲染管线了。

四.渲染管线

DirectX11学习笔记一 渲染一个三角形_第3张图片
  渲染管线就是DirectX渲染的一整套流程,流程中分为好几个步骤,有些步骤允许程序员自己编程实现。最终,你用各种原材料定义的“模型”会在屏幕上光栅化显示出来。完整的渲染管线我只知道一个大概,说不清楚具体,以后写软渲染的时候再总结。

Input-Assembler Stage

  Direct3D10和高版本的API将渲染管线分成了好几个阶段,第一个阶段就是输入组装阶段。
  输入组装阶段的目的是从内存中读取几何数据(顶点等),把这些数据组装成几何图元(比如点,线,三角形)并将其发送到顶点着色器阶段。
  顶点是以一个叫顶点缓冲区的Direct3D数据结构的形式绑定到渲染管线的,顶点缓冲区只是在连续的内存中存储了一个顶点列表。它并没有说明以何种方式组织顶点,形成几何图元。例如,是应该把顶点缓冲区中的每两个顶点解释位一条直线,还是应该把顶点缓冲区中的每三个顶点解释为一个三角形?我们通过指定图元拓扑来告诉Direct3D以何种方式组成几何图元。在新的图元拓扑被指定之前,它会一直生效下去。我们常用的图元拓扑是三角形列表(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST),就是说顶点中每三个顶点组成一个三角形。应该注意的是,三角形三个顶点的顺序非常重要,我们通过三个顶点的顺序来确定三角形哪个面是正面,为保证效率,通常我们只渲染三角形的一个面,所以顺序就决定了哪个面会被渲染出来。
DirectX11学习笔记一 渲染一个三角形_第4张图片
  使用三角形列表带来的一个问题是,构成3D物体的三角形会共享许多相同的顶点,如果我们传入每个三角形的顶点数据,那么会带来大量的额外内存需求并增加图形硬件的处理负担。为了消除这一问题,索引(index)被引入进来,假设现在有两个相邻的三角形,由于它们公用一条边,所以一共有4个顶点,顶点列表可以这样创建:

Vertex v[4] = {v0, v1, v2, v3};

  而索引列表需要定义如何将顶点列表中的顶点放在一起,构成两个三角形。

UINT indexList[6] =
{ 0, 1, 2, // Triangle 0
0, 2, 3}; // Triangle 1

  由于索引是一个简单的整数,不需要像顶点一样占用大量的内存,而且在通常情况下,适当的对顶点缓存排序,可以让图形硬件不必重复处理顶点。
  我打算结合D3D程序和渲染管线流程来学习。
  那么程序员在输入组装阶段具体需要做什么?
  没错,创建缓冲区,传入数据,绑定缓冲区到渲染管线。
  在传入数据之前,我们要先搞清楚我们要传什么数据进去。题目是渲染一个三角形,那么就要搞懂三角形如何定义。

①三角形的构成

  三角形由三个点定义,这些点又叫顶点(vertex)。确定位置的三个顶点定义了一个唯一的三角形。要让GPU绘制一个三角形,我们必须告知三个顶点的位置。例如我们想绘制一个如图所示的三角形,我们需要将顶点位置(0,0)、(0,1)和(1,0)传递到GPU,这样GPU就拥有足够的信息绘制这个三角形了。
DirectX11学习笔记一 渲染一个三角形_第5张图片
  那么如何将顶点信息传递到GPU中?在Direct3D11中,诸如位置之类的顶点信息是存储在一个缓存资源中的。存储顶点信息的缓存叫做顶点缓存(vertex buffer)。我们必须创建一个足够包含三个顶点大小的顶点缓存,并将顶点位置存储其中。在Direct3D11中,程序必须以字节为单位设置缓存大小。我们知道缓存必须大到足够包含三个顶点,那么每个顶点需要多少字节呢?要回答这个问题,需要理解顶点格式(vertex layout)

②输入格式(Input Layout)

  一个顶点具有位置信息,但往往不只包含位置信息,还可能包含发现、一个或以上的颜色、纹理坐标(用于纹理映射)等信息。顶点格式定义了这些信息属性在内存中时如何放置的;每个属性使用何种数据类型,每个属性的大小,在内存中的顺序。因为属性通常使用不同的类型,类似于C结构中的字段,所以顶点通常使用结构来表示,顶点的大小可以方便地从结构的大小获取。

struct VertexPosColor
{
	DirectX::XMFLOAT3 pos;  // 位置 x,y,z
	DirectX::XMFLOAT4 color; // 颜色 r,g,b,a
	static const D3D11_INPUT_ELEMENT_DESC inputLayout[2]; //静态常量,全局唯一,其实这个常量可以放在外面的
};

  现在我们有了表示顶点的结构体,这个结构负责将顶点信息储存到系统内存中。但是,当我们将顶点缓存传递到GPU的时候,我们传递的只是一块内存中的信息。GPU必须知道顶点格式才能从缓存中提取正确的属性,要实现这个目的,需要使用一个输入格式(input layout)
  在Direct3D11中,输入结构是一个Direct3D对象,这个对象表示可以被GPU理解的顶点结构。每个顶点属性可以由D3D11_INPUT_ELEMENT_DESC结构表示,也就是上面定义的那个。应用程序定义一个数组,这个数组包含一个或多个D3D11_INPUT_ELEMENT_DESC,然后使用这个数组创建输入格式对象,这个对象表示以整体的形式表示顶点。下面我们看一看D3D11_INPUT_ELEMENT_DESC字段的细节。

字段 描述
SemanticName 一个与元素相关的字符串。它可以是任何有效的语义名。语义(semantic)用于将顶点结构体中的元素映射为顶点着色器参数
SemanticIndex Semantic索引补充说明语义名称。一个顶点的统一特性可能有多个属性。例如,一个顶点可能有2组纹理坐标和2组颜色。你可以使用语义名称加数组的形式,例如“COLOR0”和“COLOR1”,你也可以使用语义索引0和1代替,这样两个元素可以共享相同的语义名称“COLOR”
Format Format定义了用于这个元素的数据类型。例如,DXGI_FORMAT_R32G32B32_FLOAT格式有3个32位浮点数,让元素的长度为12个字节。DXGI_FORMAT_R16G16B16A14_UINT格式有4个16位无符号整数,元素长度为8个字节。
InputSlot 前面已经提到过,一个Direct3D11程序通过使用顶点缓存将顶点数据传送到GPU中。在Direct3D11中,多个顶点缓存的数据可以同时传递到多个GPU,最多可以达到16个。每个顶点缓存绑定一个input slot数组,范围从0到15.InputSlot字段告诉GPU从哪个顶点缓存中获取这个元素。指定当前元素来自于哪个输入槽(input slot)。Direct3D支持16个输入槽(索引依次为 0到15),通过这些输入槽我们可以向着色器传入顶点数据。例如,当一个顶点由位置元素和颜色元素组成时,我们既可以使用一个输入槽传送两种元素,也可以将两种元素分开,使用第一个输入槽传送顶点元素,使用第二个输入槽传送颜色元素。Direct3D可以将来自于不同输入槽的元素重新组合为顶点。在本书中,我们只使用一个输入槽,但是在本章结尾的练习2中我们会引导读者做一个使用两个输入槽的练习。
AlignedByteOffset 对于单个输入槽来说,该参数表示从顶点结构体的起始位置到顶点元素的起始位置之间的字节偏移量。例如在下面的顶点结构体中,元素Pos的字节偏移量为0,因为它的起始位置与顶点结构体的起始位置相同;元素Normal的字节偏移量为12,因为必须跳过由Pos占用的字节才能到达Normal的起始位置;元素Tex0的字节偏移量为24,因为必须跳过由Pos和Normal占用的字节才能到达Tex0的起始位置;元素Tex1的字节偏移量为32,因为必须跳过由Pos,Normal和Tex0占用的字节才能到达Tex1的起始位置。
InputSlotClass 这个字段通常只包含D3D11_INPUT_PER_VERTEX_DATA。当程序使用instancing,可以将输入结构的InputSlotClass设置为D3D11_INPUT_PER_INSTANCE_DATA处理包含instance数据的顶点缓存。Instancing是一个高级的Direct3D内容,不会在这里讨论。在本教程中,我们只使用D3D11_INPUT_PER_VERTEX_DATA。
InstanceDataStepRate 这个字段用于instancing,因为我们不使用instancing,所以这个字段必须设置为0表示不使用。

注:关于instancing:

有时我们会多次绘制相同的物体,只是物体的位置、方向和大小有所不同(比如,将一棵树重绘多次形成一片森林)。在这种情况下,我们只需要一个相对于局部坐标系的单个副本,而不是多次赋值物体数据,为每个实例创建一个副本。当绘制物体时,我们为每个物体指定不同的世界矩阵,改变它们在世界空间中的位置、方向和大小。这种方法叫做instancing。

  简化版。

typedef struct D3D11_INPUT_ELEMENT_DESC
{
    LPCSTR SemanticName;        // 语义名
    UINT SemanticIndex;         // 语义索引
    DXGI_FORMAT Format;         // 数据格式
    UINT InputSlot;             // 输入槽索引(0-15)
    UINT AlignedByteOffset;     // 初始位置(字节偏移量)
    D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入类型
    UINT InstanceDataStepRate;  // 忽略
}   D3D11_INPUT_ELEMENT_DESC;

注:

1.语义与变量对应关系如下
DirectX11学习笔记一 渲染一个三角形_第6张图片
2.Format支持的格式

  • DXGI_FORMAT_R32_FLOAT // 1D 32-bit float scalar
  • DXGI_FORMAT_R32G32_FLOAT // 2D 32-bit float vector
  • DXGI_FORMAT_R32G32B32_FLOAT // 3D 32-bit float vector
  • DXGI_FORMAT_R32G32B32A32_FLOAT // 4D 32-bit float vector
  • DXGI_FORMAT_R8_UINT // 1D 8-bit unsigned integer scalar
  • DXGI_FORMAT_R16G16_SINT // 2D 16-bit signed integer vector
  • DXGI_FORMAT_R32G32B32_UINT // 3D 32-bit unsigned integer vector
  • DXGI_FORMAT_R8G8B8A8_SINT // 4D 8-bit signed integer vector
  • DXGI_FORMAT_R8G8B8A8_UINT // 4D 8-bit unsigned integer vector
    3.AlignedByteOffset偏移量计算方法
struct Vertex2 
{ 
   XMFLOAT3 Pos;       // 0-byte offset
   XMFLOAT3 Normal;   // 12-byte offset
   XMFLOAT2 Tex0;      // 24-byte offset
   XMFLOAT2 Tex1;      // 32-byte offset
};

  现在我们可以定义D3D11_INPUT_ELEMENT_DESC数组并创建输入格式了:

const D3D11_INPUT_ELEMENT_DESC GameApp::VertexPosColor::inputLayout[2] = {
	{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
	{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};

  代码很好理解。
  那么下一步就是创建输入格式了。
  指定了输入格式描述之后,我们就可以使用ID3D11Device::CreateInputLayout方法获取一个表示输入格式的ID3D11InputLayout接口的指针:

HRESULT ID3D11Device::CreateInputLayout( 
    const D3D11_INPUT_ELEMENT_DESC *pInputElementDescs, // [In]输入布局描述
    UINT NumElements,                                   // [In]上述数组元素个数
    const void *pShaderBytecodeWithInputSignature,      // [In]顶点着色器字节码
    SIZE_T BytecodeLength,                              // [In]顶点着色器字节码长度
    ID3D11InputLayout **ppInputLayout);                 // [Out]获取的输入布局
变量 描述
pInputElementDescs 一个用于描述顶点结构体的D3D_INPUT_ELEMENT_DESC数组。
NumElements D3D11_INPUT_ELEMENT_DESC数组的元素数量。
pShaderBytecodeWithInputSignature 指向顶点着色器参数的字节码的指针。
BytecodeLength 顶点着色器参数的字节码长度,单位为字节。
ppInputLayout 返回创建后的ID3D11InputLayout指针。

  我们需要进一步解释一下第三个参数的含义。本质上,顶点着色器以一组顶点元素作为它的输入参数——也就是所谓的输入签名(input signature)。自定义结构体中的元素必须被映射为与它们对应的顶点着色器参数,上图语义与变量关系图解释了这一问题。通过在创建输入格式时传入顶点着色器签名,Direct3D在创建时就可以验证输入格式是否与输入签名匹配,并建立从顶点结构体到着色器参数之间的映射关系。一个ID3D11InputLayout对象可以在多个参数完全相同的着色器中重复使用。
假设有下列输入参数和顶点结构:

VertexOut VS(float3 Pos:POSITION, float4 Color:COLOR,
    float3 Normal: NORMAL){ }
struct Vertex
{
    XMFLOAT3  Pos ;
    XMFLOAT4  Color;
};

  这样会产生错误,VC++的调试输出窗口会显示以下信息:

D3D11:ERROR:ID3D11Device::CreateInputLayout:The provided input
signature expects to read an element with SemanticName/Index:
‘NORMAL’/0, but the declaration doesn’t provide a matching name.

  假如顶点结构和输入参数与输入元素匹配,但类型不同:

VertexOut VS(int3 Pos:POSITION, float4 Color:COLOR) { }
struct Vertex
{
    XMFLOAT3  Pos;
    XMFLOAT4  Color;
} ;

  这样做是可行的,因为Direct3D允许输入寄存器中的字节被重新解释,但是,VC ++调试输出窗口会显示以下信息:

D3D11:WARNING:ID3D11Device::CreateInputLayout:The provided input
signature expects to read an element with SemanticName/Index:
‘POSITION’/0 and component(s) of the type ‘int32’. However,the
matching entry in the InputLayout declaration, element[0],
specifies mismatched format:‘R32G32B32_FLOAT’.This is not an error,
since behavior is well defined :The element format determines what
data conversion algorithm gets applied before it shows up in a
shader register.Independently,the shader input signature defines
how the shader will interpret the data that has been placed in its
input registers,with no change in the bits stored.It is valid for
the application to reinterpret data as a different type once it is
in the vertex shader,so this warning is issued just in case reinte rpretation was not intended by the author.

  那么接下来就是调用该方法了,方法中第三个参数代表着色器参数的指针,那么就要求我们还要创建一个着色器。假设我们已经创建了,那么CreateInputLayout调用如下。

// 创建并绑定顶点布局
	HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, 
		ARRAYSIZE(VertexPosColor::inputLayout),
		blob->GetBufferPointer(), 
		blob->GetBufferSize(), 
		m_pVertexLayout.GetAddressOf()));

  我们看到了一个新的对象,blob,定义是ComPtr blob;,那么ID3DBlob是做什么的呢?
  MSDN翻译如下:

ID3DBlob interface
这个接口用来返回任意长度的数据
方法

方法 描述
GetBufferPointer 得到指向blob数据的指针
GetBufferSize 得到blob数据的大小(以byte的形式)

注释:
ID3DBlob对象可以通过调用D3DCreateBlob方法获得。D3DCreateBlob方法包含在D3dcompiler.lib或者D3dcompiler_nn.dll中。
ID3DBlob版本相对稳定,可以使用在任何Direct3D版本的代码中。
Blob对象可用用来代指缓冲区数据。Blob还可以用来存储顶点,邻接和网格优化中的材质信息,以及其他载入操作。同样,这些对象也可以用来返回顶点,几何和像素着色器中的API错误信息。

  或许我们可以理解为Blob就是一种更抽象的指针,用来代指一些缓冲区相关的数据。
  那么现在输入格式就创建完成了,创建了输入格式对象后,它不会自动绑定到设备上,我们必须调用下面的语句来实现绑定:

ID3D11InputLayout* mVertexLayout; 
/* ...create the input layout... */
m_pd3dImmediateContext->IASetInputLayout(mVertexLayout);

  在本博文使用的代码样例中,创建输入格式在bool GameApp::InitEffect()方法,绑定输入格式在bool GameApp::InitResource()方法,不过这两个也可以放在一起。
  如果你打算用一个输入格式来绘制一些物体,然后再使用另一个的格式来绘制另一些物体,那你必须按照下面的形式来组织代码:

md3dImmediateContext->IASetInputLayout(mVertexLayout1); 
/* ...draw objects using input layout 1... */
md3dImmediateContext->IASetInputLayout(mVertexLayout2); 
/* ...draw objects using input layout 2... */

  换句话说,当一个ID3D11InputLayout对象备绑定到设备上时,如果不去改变它,那么它会始终驻留在那里。

③创建着色器

  前面我们创建输入格式的时候是假设顶点着色器已经创建好了的,那么我们现在就要创建它,事实上,创建顶点着色器应该在创建输入格式之前,我按照现在的顺序写只是为了方便理解。
  要创建顶点着色器,首先我们要先编写一个顶点着色器源码,使用HLSL语言。
  那么我们接下来就要学习编写Shader啦!
(以下笔记内容参考了很多《Unity Shader入门精要》)

事实上,我们之所以会觉得学习Shader比学习C#这样的编程语言更加困难,一个原因是因为Shader需要牵扯到整个渲染流程。当学习C++、C#这样的高级语言时,我们可以在不了解计算机架构的情况下仍然编写出实现各种功能的代码,这样的高级语言更符合人类的思维方式。然而,Shader并不是这样,Shader只是整个渲染流程中的一个子部分。虽然它很关键,但想要学习它,我们就需要了解整个渲染流程是如何进行的。和C++这样的高级语言不同,尽管Shader的编写语言已经达到了我们可以理解的程序,但Shader更多的是面向GPU的工作方式,所以它的一些语法对我们来说并不那么直观。因此,任何一篇只讲语法,不讲渲染框架的文章都无法解决读者的困惑。

  首先我们先定义顶点着色器和像素着色器中的顶点信息的结构体

struct VertexIn
{
	float3 pos : POSITION;
	float4 color : COLOR;
};

struct VertexOut
{
	float4 posH : SV_POSITION;
	float4 color : COLOR;
};

  文件用Triangle.hlsli来保存,hlsli表示HLSL头文件。当然也可以不用头文件,用到什么结构体在hlsl文件中临时定义就可以。
  接下来是顶点着色器代码

#include "Triangle.hlsli"

// 顶点着色器
VertexOut VS(VertexIn vIn)
{
    VertexOut vOut;
    vOut.posH = float4(vIn.pos, 1.0f);
    vOut.color = vIn.color; // 这里alpha通道的值默认为1.0
    return vOut;
}

  分析一下这段代码,或者可以看这个
  ①定义了一个方法叫VertexOut VS(VertexIn vIn)作为入口函数,入口函数的名字可以在hlsl文件的属性->HLSL编译器->所有选项->入口点名称 定义。这个函数会传进来一个顶点,包含了位置和颜色。那么传进来的顶点的信息是由谁在外部定义的呢?
是的,就是创建输入格式时使用的D3D11_INPUT_ELEMENT_DESC对象,你必须一一对应。
  ②创建了一个新的VertexOut对象,并拷贝了传进来的顶点的位置和颜色,然后再返回,传给像素着色器。这里有个细节。
  传出顶点的位置是4个浮点数的SV_POSITION类型数据,跟传入顶点的3个浮点数POSITION类型不同,为什么呢?
  这里就要学习一下顶点着色器和像素着色器之间的关系了。

  • 首先可以肯定的是,顶点着色器的输入是输入装配阶段传入的顶点,然后顶点着色器程序会调用每一个顶点作为输入并输出单个顶点。比如三角形三个顶点,那么顶点着色器就会调用三次(每个顶点一次),每次输入包括位置和颜色(本教程中的例子定义)。
    顶点着色器类似于
for (int i = 0; i < vertexBuffer.length; i++)
{
    executeVertexShader(vertexBuffer[i]);
}
[引用](https://blog.csdn.net/waitforfree/article/details/8579435)
  • 而输出,则是输出顶点的齐次坐标,posH,H是代表Homogeneous coordinates,也就是齐次坐标。齐次坐标是一种坐标的表示方式,举例,对于[1,2,3],你如何确定这是一个三维向量还是一个点的坐标?所以为了表示这是一个坐标,就将其表示为[1,2,3,w]。那w代表什么呢?引用下外网的资料,和这个
    DirectX11学习笔记一 渲染一个三角形_第7张图片
      首先要注意这不是表示四元数,四元数和齐次坐标是两回事。
      如果你把w按照1来处理,那你前面的三个数表示的会跟笛卡尔坐标系中的一模一样,就代表(1,2,3)这个点,所以很多情况都默认作为1来处理,包括dx,如果你传值的时候省略了一些坐标的参数,dx会自动帮你补上。但是一旦你修改了这个数,那结果就不一样了。以下是笛卡尔坐标和齐次坐标的关系。
    DirectX11学习笔记一 渲染一个三角形_第8张图片
    至于为什么要引入齐次坐标,我看了下网上说的大概是为了方便使用矩阵计算和解决透视问题,这些问题不去细究。

如果一个点在无穷远处,这个点的坐标将会(∞,∞),在欧氏空间,这变得没有意义。平行线在透视空间的无穷远处交于一点,但是在欧氏空间却不能,数学家发现了一种方式来解决这个问题。

  不靠谱的讲,对于2D图像,你可以简单的认为w是图像到透视摄像机的距离,设置为1就好了。

  So what does the W dimension do, exactly? Imagine what would happen to the 2D image if you increased or decreased W – that is, if you increased or decreased the distance between the projector and the screen. If you move the projector closer to the screen, the whole 2D image becomes smaller. If you move the projector away from the screen, the 2D image becomes larger. As you can see, the value of W affects the size (a.k.a. scale) of the image.
DirectX11学习笔记一 渲染一个三角形_第9张图片
DirectX11学习笔记一 渲染一个三角形_第10张图片

  如果是三维空间呢?
  思维的W一般是不会被修改的,当我们在矩阵变换中使用齐次坐标时,W从3D转换到4D时会被设置为1,而且变换完成后一般W还是1,从某种意义上讲你可以忽略W的将4D再转回3D。对于所有变换都是如此,旋转,缩放等等这几个最常见的变换。除了投影矩阵,投影矩阵会影响W。
  在3D图像中,透视是一种表示对象大小会随着与摄像机距离变化而变化的现象。远处的删看起来会比一只猫要小。
DirectX11学习笔记一 渲染一个三角形_第11张图片
  透视在3D图像中是使用了会修改每个顶点的W属性的变换矩阵。在摄像机矩阵应用到所有的顶点之后,但在应用投影矩阵之前,每个顶点的Z元素表示距相机的距离。 因此,Z越大,顶点应按比例缩小的越多。 W尺寸会影响比例,因此投影矩阵仅基于Z值更改W值。 这是将透视投影矩阵应用于齐次坐标的示例:
DirectX11学习笔记一 渲染一个三角形_第12张图片
  注意W是如何变成4的,受Z值影响。
  应用透视投影矩阵后,每个顶点都会进行“透视划分”。 如本文前面所述,透视除法只是将齐次坐标转换回W = 1的特定术语。 继续上面的示例,透视图划分步骤将如下所示:
在这里插入图片描述
  透视分割后,W值将被丢弃,剩下的3D坐标已根据3D透视投影正确缩放。
  在GLM中,可以使用glm :: perspective或glm :: frustum函数创建此透视投影矩阵。 在旧式OpenGL中,通常使用gluPerspective或gluFrustum函数创建它。 在OpenGL中,透视图分割是在顶点着色器在每个顶点上运行之后自动发生的。 这就是为什么顶点着色器的主要输出gl_Position是4D向量而不是3D向量的原因之一。
类似的还有方向光和点光源。
  齐次坐标的一个特性是,它们允许你在无穷远处拥有点(无限长矢量),这在3D坐标中是不可能的。 当W = 0时,出现无穷远点。 如果尝试将W = 0齐次坐标转换为普通W = 1坐标,则会导致一堆除零运算:
在这里插入图片描述
  这意味着W = 0的齐次坐标不能转换为3D坐标。
  这有什么用?定向光可以看作是无限远的点光源。 当点光源无限远时,光线会平行,并且所有光都沿单个方向传播。 这基本上是定向光的定义。
  如果W = 1,则为点光源。 如果W = 0,则它是定向光。
  因此,传统上,在3D图形中,定向光源与点光源的区别在于光源的位置矢量中的W值。 如果W = 1,则为点光源。 如果W = 0,则它是定向光。
  这更多是传统惯例,而不是一种有用的编写照明代码的方法。 方向灯和点灯通常用单独的代码实现,因为它们的行为不同。 典型的照明着色器可能如下所示:

if(lightPosition.w == 0.0){
    //directional light code here
} else {
    //point light code here
}

  这些权当是科普了,毕竟投影变换裁剪之类的这些东西不是顶点和像素着色器需要考虑的东西。
  但是,旋转,缩放,平移这些是我们可以在顶点着色器对顶点进行的操作。下面我们就学习一下。可以参考这个网站。
  限于篇幅,不再去细看了,如果后面有机会,我单独开一篇博客,好好琢磨琢磨。

综上所述,要获得从对象(又称局部)空间到投影空间的3d模型,我们将每个顶点乘以世界,然后是视图,然后是投影。它看起来像这样:
finalvertex.pos = vertex.pos * worldMatrix * viewMatrix * projectionMatrix;
在我们的顶点着色器中,我们实际上将使用世界/视图/投影矩阵(包含所有三个空间的单个矩阵)将每个顶点移入投影空间,如下所示:
output.pos = mul(input.pos, wvpMat); wvp代表后三个矩阵的缩写。或叫做MVP变换(model->view->projection).
但是渲染三角形比较简单,可以暂时忽略wvp,后面学渲染立方体的时候再说。

  最后一个问题,为什么是SV_POSITION而不是POSITION?SV代表System value,系统变量,参考这里,另外我们可以看到“When used in a shader, SV_Position describes the pixel location. ”,后面会用到。
好,那么现在顶点着色器的作用和输入输出大体上有了一个了解,那像素着色器呢?

#include "Cube.hlsli"
// 像素着色器
float4 PS(VertexOut pIn) : SV_Target
{
    return pIn.color;
}

  像素着色器从抽象的角度来讲,可以理解为是,fragment代表顶点之间围起来的像素,像素着色器遍历所有未着色的像素,然后调用该程序。

for (var i:int = 0; i < fragmentStream.length; i++)
{
    executeFragmentShader(fragmentStream[i]);
}//https://blog.csdn.net/waitforfree/article/details/8579435

片段着色器真正位于可编程图形管道的核心。片段着色器的最常见用途是计算从顶点属性颜色(用于顶点着色几何体)或从纹理和相关的顶点属性 UV 纹理坐标(用于纹理几何体)开始的各种三角形像素颜色。
但片段着色器并不仅限于创建这些简单效果,实际上,片段着色器可用于创建您在现代 3D 游戏中看到的所有令人惊艳的 3D 效果。例如,动态光线效果大部分都是 使用片段着色器完成的。可以想象,动态光线意味着依据场景中存在的光线、它们相对于我们的几何体的位置,以及几何体的材质来计算像素颜色。这正是动态光线 最常使用片段着色器创建的原因。
反射效果,比如水或环境映射,都是使用片段着色器创建的。可使用片段着色器创建的效果非常丰富,这些基本效果仅仅是所有可能性的冰山一角。
最后,您在屏幕上看到效果取决于片段着色器。所以片段着色器是管理实际呈现的内容的代码。

  像素着色器完全是二维数据,跟顶点着色器的矩阵变换完全是两种思维模式。

  • 输入,像素着色器的输入,参考这个网站,不过看了半天还是很迷糊,也许我们需要先去恶补一下光栅化的原理,毕竟从API开始学图形学并不是一个最好的办法。。。不过上面有看到“When used in a shader, SV_Position describes the pixel location. ”,那像素着色器的输入显然就是像素位置了。原理很明显,顶点着色器就是计算顶点位置或者顶点光照之类的与顶点有关的计算,而像素着色器,完全是发生在光栅化之后的事情,这时像素着色器的主要任务就是往三个顶点围起来的三角形内部涂色,使用扫描线算法,输入的变量是三个三角形顶点插值的结果,所以它的值是渐变的。插值原理是顶点属性插值,先扫盲,后面再回来详细分析原理。
  • 输出,显而易见,大概率就是像素最终的颜色了。

  好,到现在,着色器我们都写完了,接下来就是在D3D中编译源文件、创建着色器资源并绑定到渲染管线

bool GameApp::InitEffect()
{
	ComPtr<ID3DBlob> blob;

	// 创建顶点着色器
	HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf()));
	// 创建并绑定顶点布局
	HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, 
		ARRAYSIZE(VertexPosColor::inputLayout),
		blob->GetBufferPointer(), 
		blob->GetBufferSize(), 
		m_pVertexLayout.GetAddressOf()));

	// 创建像素着色器
	HR(CreateShaderFromFile(L"HLSL\\Triangle_PS.cso", L"HLSL\\Triangle_PS.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
	HR(m_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), 
	blob->GetBufferSize(), 
	nullptr, 
	m_pPixelShader.GetAddressOf()));

	return true;
}

HRESULT CreateShaderFromFile(  //主要是编译源文件
	const WCHAR* csoFileNameInOut,
	const WCHAR* hlslFileName,
	LPCSTR entryPoint,
	LPCSTR shaderModel,
	ID3DBlob** ppBlobOut)
{
	HRESULT hr = S_OK;

	// 寻找是否有已经编译好的顶点着色器
	if (csoFileNameInOut && D3DReadFileToBlob(csoFileNameInOut, ppBlobOut) == S_OK)
	{
		return hr;
	}
	else
	{
		DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
		// 设置 D3DCOMPILE_DEBUG 标志用于获取着色器调试信息。该标志可以提升调试体验,
		// 但仍然允许着色器进行优化操作
		dwShaderFlags |= D3DCOMPILE_DEBUG;

		// 在Debug环境下禁用优化以避免出现一些不合理的情况
		dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
		ID3DBlob* errorBlob = nullptr;
		hr = D3DCompileFromFile(hlslFileName, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entryPoint, shaderModel,
			dwShaderFlags, 0, ppBlobOut, &errorBlob);
		if (FAILED(hr))
		{
			if (errorBlob != nullptr)
			{
				OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
			}
			SAFE_RELEASE(errorBlob);
			return hr;
		}

		// 若指定了输出文件名,则将着色器二进制信息输出
		if (csoFileNameInOut)
		{
			return D3DWriteBlobToFile(*ppBlobOut, csoFileNameInOut, FALSE);
		}
	}

	return hr;
}

CreatePixelShader方法
CreateVertexShader方法

④在输入装配阶段传入模型资源

  • 第一步,创建顶点缓冲区,与上面创建其他对象的步骤类似,都需要先有一个XXX描述(布局/格式)之类的对象作为规定,然后调用创建方法。
// 设置顶点缓冲区描述
   D3D11_BUFFER_DESC vbd;
   ZeroMemory(&vbd, sizeof(vbd));
   vbd.Usage = D3D11_USAGE_IMMUTABLE;  //表示所有数据均由初始化时定义,运行时无法修改
   vbd.ByteWidth = sizeof vertices;  //顶点数据大小
   vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER;  //顶点着色器
   vbd.CPUAccessFlags = 0;  //0表示不需要CPU的访问
typedef struct D3D11_BUFFER_DESC
   {
   UINT ByteWidth;  //我们将要创建的顶点缓冲区的大小,单位为字节。
   D3D11_USAGE Usage;  //一个用于指定缓冲区用途的D3D11_USAGE枚举类型成员。有4个可选值
   UINT BindFlags;  //对于顶点缓冲区,该参数应设为D3D11_BIND_VERTEX_BUFFER。
   UINT CPUAccessFlags;  //指定CPU对资源的访问权限。设置为0则表示CPU无需读写缓冲。如果CPU需要向资源写入数据,则应指定D3D11_CPU_ACCESS_WRITE。具有写访问权限的资源的Usage参数应设为D3D11_USAGE_DYNAMIC或D3D11_USAGE_STAGING。如果CPU需要从资源读取数据,则应指定D3D11_CPU_ACCESS_READ。具有读访问权限的资源的Usage参数应设为D3D11_USAGE_STAGING。当指定这些标志值时,应按需而定。通常,CPU从Direct3D资源读取数据的速度较慢。CPU向资源写入数据的速度虽然较快,但是把内存副本传回显存的过程仍很耗时。所以,最好的做法是(如果可能的话)不指定任何标志值,让资源驻留在显存中,只用GPU来读写数据。
   UINT MiscFlags;  //我们不需要为顶点缓冲区指定任何杂项(miscellaneous)标志值,所以该参数设为0。有关D3D11_RESOURCE_MISC_FLAG枚举类型的详情请参阅SDK文档。
   UINT StructureByteStride;  //存储在结构化缓冲中的一个元素的大小,以字节为单位。这个属性只用于结构化缓冲,其他缓冲可以设置为0。所谓结构化缓冲,是指存储其中的元素大小都相等的缓冲。
   } 	

  D3D11_BUFFER_DESC官网给的描述很少,目前需要我们改动的地方也很少,有兴趣可以查MSDN

Usage 描述
D3D10_USAGE_DEFAULT 表示GPU会对资源执行读写操作。在使用映射API(例如ID3D11DeviceContext::Map)时,CPU在使用映射API时不能读写这种资源,但它能使用ID3D11DeviceContext::UpdateSubresource。
D3D11_USAGE_IMMUTABLE 表示在创建资源后,资源中的内容不会改变。这样可以获得一些内部优化,因为GPU会以只读方式访问这种资源。除了在创建资源时CPU会写入初始化数据外,其他任何时候CPU都不会对这种资源执行任何读写操作,我们也无法映射或更新一个只读资源。
D3D11_USAGE_DYNAMIC 表示应用程序(CPU)会频繁更新资源中的数据内容(例如,每帧更新一次)。GPU可以从这种资源中读取数据,使用映射API(ID3D11DeviceContext::Map)时,CPU可以向这种资源中写入数据。因为新的数据要从CPU内存(即系统RAM)传送到GPU内存(即显存),所以从CPU动态地更新GPU资源会有性能损失;若非必须,请勿使用D3D11_USAGE_DYNAMIC。
D3D11_USAGE_STAGING 表示应用程序(CPU)会读取该资源的一个副本(即,该资源支持从显存到系统内存的数据复制操作)。显存到系统内存的复制是一个缓慢的操作,应尽量避免。使用ID3D11DeviceContext::CopyResource和ID3D11DeviceContext::CopySubresourceRegion方法可以复制资源,在12.3.5节会介绍一个复制资源的例子。

  vertices是我们定义的三角形顶点

	// 设置三角形顶点
	VertexPosColor vertices[] =
	{
		{ XMFLOAT3(0.0f, 0.5f, 0), XMFLOAT4(0.0f, 1.0f, 0.0f, 1.0f) },
		{ XMFLOAT3(0.5f, -0.5f, 0), XMFLOAT4(0.0f, 0.0f, 1.0f, 1.0f) },
		{ XMFLOAT3(-0.5f, -0.5f, 0), XMFLOAT4(1.0f, 0.0f, 0.0f, 1.0f) }
	};

  这个坐标系是按照1.0是到达屏幕边缘计算的(其实就是个比例值),颜色是1.0代表255,传进去之后这些值都会进行转换。
  创建顶点缓冲区

	// 新建顶点缓冲区
	D3D11_SUBRESOURCE_DATA InitData;
	ZeroMemory(&InitData, sizeof(InitData));
	InitData.pSysMem = vertices;
	HR(m_pd3dDevice->CreateBuffer(&vbd, &InitData, m_pVertexBuffer.GetAddressOf()));

  其中InitData只是转存顶点数据,用来创建缓冲区m_pVertexBuffer。定义如下

typedef struct D3D11_SUBRESOURCE_DATA { 
    const void *pSysMem;   //包含初始化数据的系统内存数组的指针。当缓冲区可以存储n个顶点时,对应的初始化数组也应至少包含n个顶点,从而使整个缓冲区得到初始化。
    UINT SysMemPitch;   //顶点缓冲区不使用该参数。
    UINT SysMemSlicePitch;   //顶点缓冲区不使用该参数。
} D3D11_SUBRESOURCE_DATA;
  • 第二步,将顶点缓冲区与渲染管线绑定。
// ******************
	// 给渲染管线各个阶段绑定好所需资源
	//

	// 输入装配阶段的顶点缓冲区设置
	UINT stride = sizeof(VertexPosColor);	// 跨越字节数
	UINT offset = 0;						// 起始偏移量

	m_pd3dImmediateContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &stride, &offset);
	// 设置图元类型,设定输入布局
	m_pd3dImmediateContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
	m_pd3dImmediateContext->IASetInputLayout(m_pVertexLayout.Get());
	// 将着色器绑定到渲染管线
	m_pd3dImmediateContext->VSSetShader(m_pVertexShader.Get(), nullptr, 0);
	m_pd3dImmediateContext->PSSetShader(m_pPixelShader.Get(), nullptr, 0);
void ID3D11DeviceContext::IASetVertexBuffers( 
    UINT StartSlot,   //顶点缓冲区所要绑定的起始输入槽。一共有16个输入槽,索引依次为0到15。
    UINT NumBuffers,   //顶点缓冲区所要绑定的输入槽的数量,如果起始输入槽为索引k,我们绑定了n个缓冲,那么缓冲将绑定在索引为Ik,Ik+1……Ik+n-1的输入槽上。
    ID3D10Buffer *const *ppVertexBuffers,   //指向顶点缓冲区数组的第一个元素的指针。
    const UINT *pStrides,   //指向步长数组的第一个元素的指针(该数组的每个元素对应一个顶点缓冲区,也就是,第i个步长对应于第i个顶点缓冲区)。这个步长是指顶点缓冲区中的元素的字节长度。
    const UINT *pOffsets);  //指向偏移数组的第一个元素的指针(该数组的每个元素对应一个顶点缓冲区,也就是,第i个偏移量对应于第i个顶点缓冲区)。这个偏移量是指从顶点缓冲区的起始位置开始,到输入装配阶段将要开始读取数据的位置之间的字节长度。当希望跳过顶点缓冲区前面的一部分数据时,可以使用该参数。

  因为IASetVertexBuffers方法支持将一个顶点缓冲数组设置到不同的输入槽中,因此这个方法看起来有点复杂。但是,大多数情况下我们只使用一个输入槽。

  关于IASetPrimitiveTopology,参阅D3D_PRIMITIVE_TOPOLOGY Enumeration

⑤渲染三角形

void GameApp::DrawScene()  
{
	assert(m_pd3dImmediateContext);
	assert(m_pSwapChain);

	static float black[4] = { 0.0f, 0.0f, 0.0f, 1.0f };	// RGBA = (0,0,0,255)
	m_pd3dImmediateContext->ClearRenderTargetView(m_pRenderTargetView.Get(), black);
	m_pd3dImmediateContext->ClearDepthStencilView(m_pDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	// 绘制三角形
	// [In]需要绘制的顶点数目
	// [In]起始顶点索引
	m_pd3dImmediateContext->Draw(3, 0);  //根据已经绑定的顶点缓冲区进行绘制,该方法不需要提供索引缓冲区
	HR(m_pSwapChain->Present(0, 0));
}

  Draw方法定义
  好了,到此,执行即可渲染出一个彩色渐变三角形。

  下一个笔记看渲染立方体。

你可能感兴趣的:(DirectX)