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
头文件中,并且通常与 D3DCompile
或 D3DCompileFromFile
函数一起使用,这些函数是用于编译着色器的主要函数。
下面是 ID3DInclude
接口中定义的主要方法:
-
Open:当编译器遇到
#include
指令时,它会调用此方法。应用程序应该实现此方法以打开指定的文件,并返回一个指向文件内容的指针。 -
Close:在编译器完成处理一个包含的文件后,它会调用此方法。应用程序应该实现此方法以关闭文件并释放任何关联的资源。
需要注意的是,仅包含这两个函数的版本是用于D3D12配套版本的着色器编译器的。
3、基本使用方法
为了使用自定义的包含逻辑,你需要实现一个类,该类继承自 ID3DInclude
接口,并实现上述所有方法。然后,在调用 D3DCompile
或 D3DCompileFromFile
时,你可以将你的实现作为参数传递。
下面是一个简单的示例,展示了如何实现 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
时,我们将 myIncludeInstance
(MyInclude
类的一个实例)作为参数传递,以便使用自定义的包含逻辑。
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();