使用D3DCompileFromFile编译HLSL时包含文件路径错误解决方案咨询
我正在开发一个基于DirectX 11的程序,使用HLSL编写着色器,文件结构如下:
global.hlsl lighting.hlsl folderA/shader1VS.hlsl folderA/shader1PS.hlsl
global.hlsl:定义DirectionalLight、PointLight等结构体及灯光相关常量缓冲区(cbuffer);lighting.hlsl:包含灯光计算函数,通过#include "global.hlsl"引用global.hlsl;shader1PS.hlsl:通过#include "../global.hlsl"和#include "../lighting.hlsl"引用前两者。
在VS2022中点击「构建解决方案」时,内置着色器编译功能正常,但调用D3DCompileFromFile函数编译shader1PS.hlsl时,报错:failed to open source file 'global.hlsl'。
推测原因:编译时的包含处理会将被包含文件代码直接插入主文件,此时lighting.hlsl里的#include "global.hlsl"路径以shader1PS.hlsl所在的folderA目录为基准,而global.hlsl实际在根目录,因此无法找到文件。现寻求不修改文件目录结构的解决方法。
EDIT1:补充代码片段
C++编译像素着色器代码
DxResPtr<ID3DBlob> pixelShaderBlob, errorBlob; HRESULT result; D3D_SHADER_MACRO globalMacros[] = { // 自定义宏定义 NULL, NULL }; if (!pixelShaderFilePath.empty()) { result = D3DCompileFromFile(pixelShaderFilePath.c_str(), globalMacros, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "ps_5_0", shaderCompilationFlags, 0, pixelShaderBlob.reset(), errorBlob.reset()); if(FAILED(result)) { if(errorBlob) { OutputDebugStringA((char*)errorBlob->GetBufferPointer()); } } result = g_device->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), NULL, m_pixelShader.reset()); }
HLSL文件代码
global.hlsl
#ifndef __GLOBAL_HLSL__ #define __GLOBAL_HLSL__ // 结构体、常量缓冲区、工具函数定义 #endif
lighting.hlsl
#ifndef __LIGHTING_HLSL__ #define __LIGHTING_HLSL__ #include "global.hlsl" // 依赖global.hlsl中结构体的灯光计算函数 #endif
shader1PS.hlsl
#ifndef __SHADER1PS_HLSL__ #define __SHADER1PS_HLSL__ #include "../global.hlsl" #include "../lighting.hlsl" // 使用global.hlsl和lighting.hlsl中内容的像素着色器主函数 #endif
问题核心是D3D_COMPILE_STANDARD_FILE_INCLUDE(标准包含处理器)处理嵌套#include时,仅以主编译文件(shader1PS.hlsl)的目录为基准解析相对路径,不会切换到被包含文件(lighting.hlsl)的目录。以下是两种无需修改文件目录结构的解决方法:
方法一:实现自定义ID3DInclude接口
自定义包含处理器,让它在处理#include时,自动根据被包含文件的所在目录解析相对路径,同时支持添加额外搜索目录(如项目根目录)。
实现代码示例
#include <d3dcompiler.h> #include <string> #include <filesystem> #include <fstream> #include <vector> #include <windows.h> namespace fs = std::filesystem; class CustomIncludeHandler : public ID3DInclude { public: CustomIncludeHandler(const fs::path& mainFilePath) : m_mainDir(mainFilePath.parent_path()) {} STDMETHODIMP Open(D3D_INCLUDE_TYPE IncludeType, LPCSTR pFileName, LPCVOID pParentData, LPCVOID* ppData, UINT* pBytes) override { fs::path filePath; if (IncludeType == D3D_INCLUDE_LOCAL) { // 嵌套包含时,以父文件目录为基准 if (pParentData) { const fs::path* parentDir = static_cast<const fs::path*>(pParentData); filePath = *parentDir / pFileName; } // 主文件的include,以主文件目录为基准 else { filePath = m_mainDir / pFileName; } } else { // 系统包含(<xxx>),这里指定根目录为搜索路径 filePath = m_mainDir.parent_path() / pFileName; } // 读取文件内容 std::ifstream file(filePath, std::ios::binary | std::ios::ate); if (!file.is_open()) { return E_FAIL; } UINT fileSize = static_cast<UINT>(file.tellg()); char* buffer = new char[fileSize]; file.seekg(0); file.read(buffer, fileSize); *ppData = buffer; *pBytes = fileSize; // 保存当前文件目录,作为后续嵌套include的父目录 m_includedFiles.emplace_back(filePath.parent_path(), buffer); return S_OK; } STDMETHODIMP Close(LPCVOID pData) override { for (auto it = m_includedFiles.begin(); it != m_includedFiles.end(); ++it) { if (it->second == pData) { delete[] static_cast<const char*>(pData); m_includedFiles.erase(it); break; } } return S_OK; } // COM接口必需实现 STDMETHODIMP_(ULONG) AddRef() override { return InterlockedIncrement(&m_refCount); } STDMETHODIMP_(ULONG) Release() override { ULONG count = InterlockedDecrement(&m_refCount); if (count == 0) delete this; return count; } STDMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override { if (riid == __uuidof(ID3DInclude) || riid == __uuidof(IUnknown)) { *ppvObject = static_cast<ID3DInclude*>(this); AddRef(); return S_OK; } *ppvObject = nullptr; return E_NOINTERFACE; } private: fs::path m_mainDir; std::vector<std::pair<fs::path, const char*>> m_includedFiles; LONG m_refCount = 1; };
使用方式
调用D3DCompileFromFile时,替换第三个参数为自定义包含处理器:
if (!pixelShaderFilePath.empty()) { CustomIncludeHandler* includeHandler = new CustomIncludeHandler(pixelShaderFilePath); result = D3DCompileFromFile(pixelShaderFilePath.c_str(), globalMacros, includeHandler, "main", "ps_5_0", shaderCompilationFlags, 0, pixelShaderBlob.reset(), errorBlob.reset()); includeHandler->Release(); // 释放COM对象 if(FAILED(result)) { if(errorBlob) { OutputDebugStringA((char*)errorBlob->GetBufferPointer()); } } // 创建像素着色器的代码 }
该处理器会在嵌套包含时自动切换到被包含文件的目录解析路径,完全匹配VS内置编译的行为。
方法二:临时切换工作目录
简单粗暴的方法:编译前将程序工作目录切换到项目根目录,让所有相对路径以根目录为基准,编译完成后恢复原目录。
#include <windows.h> #include <filesystem> namespace fs = std::filesystem; if (!pixelShaderFilePath.empty()) { // 保存当前工作目录 WCHAR oldDir[MAX_PATH]; GetCurrentDirectory(MAX_PATH, oldDir); // 切换到项目根目录(根据实际路径调整) fs::path rootDir = fs::path(pixelShaderFilePath).parent_path().parent_path(); SetCurrentDirectory(rootDir.c_str()); // 编译着色器 result = D3DCompileFromFile(pixelShaderFilePath.c_str(), globalMacros, D3D_COMPILE_STANDARD_FILE_INCLUDE, "main", "ps_5_0", shaderCompilationFlags, 0, pixelShaderBlob.reset(), errorBlob.reset()); // 恢复原工作目录 SetCurrentDirectory(oldDir); if(FAILED(result)) { if(errorBlob) { OutputDebugStringA((char*)errorBlob->GetBufferPointer()); } } // 创建像素着色器的代码 }
这种方法无需编写复杂COM接口,但要注意工作目录切换可能影响其他文件操作,必须确保编译后恢复原目录。
内容的提问来源于stack exchange,提问作者OftenDeadKanji

