You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 13:14:54