1、前言

  本章教程较短小,但内容十分重要,是后续更灵活使用 Shader 编程的重要基础之一。也就是对 Shader 代码进行头文件分离复用设计进行全面支持。或者直白的说,本章内容重点就是让各位掌握以编程的方式在代码中支持 Shader 头文件的方法,方便在设计 Shader 编辑器之类工具时,可以让所有的 Shader 组织的更有层次。本章实例代码依然基于示例 GRSD3D12Sample/25-IBL-MultiInstance-Sphere

2、ID3DInclude 回调接口介绍

  这个接口是个很有意思的接口,名字看上去是很 COM 化的,但其实跟 COM 没啥关系,只是一个定义了两个纯虚接口函数的类而已,其详细定义摘录如下:

typedef interface ID3DInclude ID3DInclude;
#undef INTERFACE
#define INTERFACE ID3DInclude
DECLARE_INTERFACE(ID3DInclude)
{
    STDMETHOD(Open)(THIS_ D3D_INCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID *ppData, UINT *pBytes) PURE;
    STDMETHOD(Close)(THIS_ LPCVOID pData) PURE;
};
typedef ID3DInclude* LPD3DINCLUDE;

  ID3DInclude 是从Direct3D 10及更高版本中引入的回调接口,它用于处理着色器编译过程中的文件包含操作,即 “#include” 宏命令。在着色器代码中,我们经常需要包含其他文件,例如头文件,来共享常用的函数或变量定义。ID3DInclude 接口允许应用程序为着色器编译器提供自定义的文件包含逻辑。

  ID3DInclude 接口定义在 d3dcompiler.h 头文件中,并且通常与 D3DCompileD3DCompileFromFile 函数一起使用,这些函数是用于编译着色器的主要函数。

  下面是 ID3DInclude 接口中定义的主要方法:

  • Open:当编译器遇到 #include 指令时,它会调用此方法。应用程序应该实现此方法以打开指定的文件,并返回一个指向文件内容的指针。

  • Close:在编译器完成处理一个包含的文件后,它会调用此方法。应用程序应该实现此方法以关闭文件并释放任何关联的资源。

      需要注意的是,仅包含这两个函数的版本是用于D3D12配套版本的着色器编译器的。

3、基本使用方法

  为了使用自定义的包含逻辑,你需要实现一个类,该类继承自 ID3DInclude 接口,并实现上述所有方法。然后,在调用 D3DCompileD3DCompileFromFile 时,你可以将你的实现作为参数传递。

  下面是一个简单的示例,展示了如何实现 ID3DInclude 接口:

#include <d3dcompiler.h>
class MyInclude : public ID3DInclude
{
public:
  virtual HRESULT STDMETHODCALLTYPE Open(D3D_INCLUDE_TYPE IncludeType, 
                      const char* pFileName, 
                      const void* pParentData, 
                      const void** ppData, 
                      UINT* pBytes) override

  {
    // 在这里实现你的文件打开逻辑
    // ...
    return S_OK;
  }

  virtual HRESULT STDMETHODCALLTYPE Close(const void* pData) override
  {
    // 在这里实现你的文件关闭逻辑
    // ...
    return S_OK;
  }
};

// 使用自定义的包含逻辑编译着色器
ID3DBlob* compiledShader = nullptr;
HRESULT hr = D3DCompileFromFile(shaderFilePath, nullptr, nullptr, "VSMain", "vs_5_0", 
                0, 0, &compiledShader, nullptr, &myIncludeInstance);

  在上面的示例中,MyInclude 类实现了 ID3DInclude 接口,并在构造函数中接受一个搜索路径参数。你可以根据需要在 Open 方法中实现自定义的文件搜索和打开逻辑。然后,在调用 D3DCompileFromFile 时,我们将 myIncludeInstanceMyInclude 类的一个实例)作为参数传递,以便使用自定义的包含逻辑。

4、示例代码中的实现

GRSD3D12Sample/25-IBL-MultiInstance-Sphere 及系列示例中,具体实现这个接口如下:

#pragma once
#include <SDKDDKVer.h>
#include <tchar.h>
#define WIN32_LEAN_AND_MEAN // 从 Windows 头中排除极少使用的资料
#include <windows.h>
#include <strsafe.h>

#include <atlconv.h>
#include <atlcoll.h>
#include <atlstr.h>
#include <d3dcompiler.h>

#include "GRS_Mem.h"

#define GRS_FILE_IS_EXIST(f) (INVALID_FILE_ATTRIBUTES != ::GetFileAttributes(f))

#ifndef GRS_SAFE_CLOSEFILE
//安全关闭一个文件句柄
#define GRS_SAFE_CLOSEFILE(h) if(INVALID_HANDLE_VALUE != (h)){::CloseHandle(h);(h)=INVALID_HANDLE_VALUE;}
#endif // !GRS_SAFE_CLOSEFILE

// 强制D3DCompiler搜索标准的Include目录,通常这也没啥用,只是保留记得有这么个设置即可
#ifndef D3D_COMPILE_STANDARD_FILE_INCLUDE
#define D3D_COMPILE_STANDARD_FILE_INCLUDE ((ID3DInclude*)(UINT_PTR)1)
#endif

typedef CAtlArray<CAtlString> CStringList;

class CGRSD3DCompilerInclude final : public ID3DInclude
{
protected:
	CStringList m_DirList;
public:
	CGRSD3DCompilerInclude(LPCTSTR pszDir)
	{
		AddDir(pszDir);
	}

	virtual ~CGRSD3DCompilerInclude()
	{
	}
public:
	VOID AddDir(LPCTSTR pszDir)
	{
		CString strDir(pszDir);
		if ( ( strDir.GetLength() > 0 ) )
		{ //非空路径
			if (_T('\\') == strDir[strDir.GetLength() - 1])
			{
				strDir.SetAt(strDir.GetLength() - 1, _T('\0'));
			}

			for ( size_t i = 0; i < m_DirList.GetCount(); i++ )
			{
				if ( m_DirList[i] == strDir )
				{// 已经存在不用再添加了
					return;
				}
			}

			m_DirList.Add(strDir);
		}
	}
public:
	STDMETHOD(Open)(THIS_ D3D_INCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID* ppData, UINT* pBytes) override
	{
		HANDLE hFile = INVALID_HANDLE_VALUE;
		VOID* pFileData = nullptr;
		*ppData = nullptr;
		HRESULT _hrRet = S_OK;
		try
		{ 
			CString strFileName(pFileName);
			TCHAR pszFullFileName[MAX_PATH] = {};

			for (size_t i = 0; i < m_DirList.GetCount(); i++)
			{
				HRESULT hr = ::StringCchPrintf(pszFullFileName
					, MAX_PATH
					, _T("%s\\%s")
					, m_DirList[i].GetBuffer()
					, strFileName.GetBuffer() );

				if ( FAILED(hr) )
				{
					AtlThrow(hr);
				}

				if ( !GRS_FILE_IS_EXIST(pszFullFileName) )
				{
					// 记录下这个错误,直到最后真找不到那么就作为最后的错误码返回
					_hrRet = HRESULT_FROM_WIN32(::GetLastError());
					continue;
				}

				hFile = ::CreateFile(pszFullFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

				if ( INVALID_HANDLE_VALUE == hFile )
				{
					AtlThrowLastWin32();
				}

				DWORD dwFileSize = ::GetFileSize(hFile, NULL);//Include文件一般不会超过4G大小,所以......

				if (INVALID_FILE_SIZE == dwFileSize)
				{
					AtlThrowLastWin32();
				}

				VOID* pFileData = GRS_CALLOC(dwFileSize);
				if (nullptr == pFileData)
				{
					AtlThrowLastWin32();
				}

				if (!::ReadFile(hFile, pFileData, dwFileSize, (LPDWORD)pBytes, NULL))
				{
					AtlThrowLastWin32();
				}
				GRS_SAFE_CLOSEFILE(hFile);

				*ppData = pFileData;
				_hrRet = S_OK;
				break;				
			}
		}
		catch (CAtlException& e)
		{
			GRS_SAFE_CLOSEFILE(hFile);
			GRS_SAFE_FREE(pFileData);
			_hrRet = e.m_hr;
		}
		GRS_SAFE_CLOSEFILE(hFile);
		return _hrRet;
	}

	STDMETHOD(Close)(THIS_ LPCVOID pData) override
	{
		GRS_FREE((VOID*)pData);
		return S_OK;
	}
};

  上面代码中,只是加入了一个预编译文件可能路径的支持,即用一个字符串对象的链表来存储所有潜在路径,在需要打开头文件时,就顺序搜索这个字符串列表,直到找到指定的头文件为止。

  在实际使用时,就是在程序开头处,将所有的包含路径都加入到该对象中,然后编译时直接使用即可。

// 0-2、得到当前的工作目录,方便我们使用相对路径来访问各种资源文件
{
    if (0 == ::GetModuleFileName(nullptr, g_pszAppPath, MAX_PATH))
    {
        GRS_THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()));
    }

    WCHAR* lastSlash = _tcsrchr(g_pszAppPath, _T('\\'));
    if (lastSlash)
    {//删除Exe文件名
        *(lastSlash) = _T('\0');
    }

    lastSlash = _tcsrchr(g_pszAppPath, _T('\\'));
    if (lastSlash)
    {//删除x64路径
        *(lastSlash) = _T('\0');
    }

    lastSlash = _tcsrchr(g_pszAppPath, _T('\\'));
    if (lastSlash)
    {//删除Debug 或 Release路径
        *(lastSlash + 1) = _T('\0');
    }

    // Shader 路径
    ::StringCchPrintf(g_pszShaderPath
        , MAX_PATH
        , _T("%s25-IBL-MultiInstance-Sphere\\Shader")
        , g_pszAppPath);

    // 资源路径
    ::StringCchPrintf(g_pszAssetsPath
        , MAX_PATH
        , _T("%sAssets")
        , g_pszAppPath);
}

// 0-3、准备编程Shader时处理包含文件的类及路径        
TCHAR pszPublicShaderPath[MAX_PATH] = {};
::StringCchPrintf(pszPublicShaderPath
    , MAX_PATH
    , _T("%sShader")
    , g_pszAppPath);
CGRSD3DCompilerInclude grsD3DCompilerInclude(pszPublicShaderPath);
grsD3DCompilerInclude.AddDir(g_pszShaderPath);

需要编译shader时:

ComPtr<ID3DBlob> pIVSCode;
ComPtr<ID3DBlob> pIGSCode;
ComPtr<ID3DBlob> pIPSCode;
ComPtr<ID3DBlob> pIErrorMsg;

::StringCchPrintf(pszShaderFileName
    , MAX_PATH
    , _T("%s\\GRS_1Times_GS_HDR_2_CubeMap_VS_GS.hlsl"), g_pszShaderPath);

HRESULT hr = D3DCompileFromFile(pszShaderFileName, nullptr, &grsD3DCompilerInclude
    , "VSMain", "vs_5_0", nCompileFlags, 0, &pIVSCode, &pIErrorMsg);

if (FAILED(hr))
{
    ATLTRACE("编译 Vertex Shader:\"%s\" 发生错误:%s\n"
        , T2A(pszShaderFileName)
        , pIErrorMsg ? pIErrorMsg->GetBufferPointer() : "无法读取文件!");
    GRS_THROW_IF_FAILED(hr);
}
pIErrorMsg.Reset();
04-24 13:46