c++string

How can I use #include in a GLSL file using C++


I want to use the #include directive in GLSL files so I don’t have to rewrite the same code for every GLSL shader I create. I tried to create my own small parser for this purpose, but I couldn’t find a good way to locate all #include statements in a GLSL file. I want to extract the paths of the included files, read them, and write their content into the actual shader (not the original GLSL file that uses the #include directive, but the content read into a std::string).

I attempted to find shader includes using the following code:

std::string include = "#include";
std::string shaderSource = ReadFile("shader.glsl");
std::uint32_t offset = 0, position = 0;
while ((position = shaderSource.find(include, offset)) != std::string::npos)
{
    offset += include.size();
    // parse the shader
}

However, I realize that this approach may not work in all scenarios, and I believe the solution should be more complex than what I've implemented.


Solution

  • Note: This version is done with C++ code, as you asked for in your comment on my other answer.

    The idea is to search for text of the format #include "filename" and to recursively replace that text with the contents of the included file.

    I've given some consideration to specifying additional include directories. But I've also taken some shortcuts sacrificing robustness, particularly with the regular expression for matching "" or <>.

    This version takes a string as its original input (not a file). If you want to take input from a file, you can just process a string with a single #include directive that includes the desired file, or extend this implementation as you desire to read in the original file first.

    #include <iostream>
    #include <fstream>
    #include <sstream>
    #include <string>
    #include <regex>
    #include <vector>
    #include <filesystem>
    
    // Read a file and return its contents as a string
    std::string readFile(const std::string& fileName) {
        std::ifstream file(fileName);
        if (!file.is_open()) {
            throw std::runtime_error("Could not open file: " + fileName);
        }
    
        std::stringstream buffer;
        buffer << file.rdbuf();
        return buffer.str();
    }
    
    // Find the file in the current directory or in additional include directories
    std::string findIncludeFile(const std::string& fileName, const std::vector<std::string>& includeDirs) {
        if (std::filesystem::exists(fileName)) {
            return fileName; // File found in the current directory
        }
    
        for (const auto& dir : includeDirs) {
            std::filesystem::path filePath = std::filesystem::path(dir) / fileName;
            if (std::filesystem::exists(filePath)) {
                return filePath.string(); // File found in one of the include directories
            }
        }
    
        throw std::runtime_error("File not found: " + fileName);
    }
    
    // Process `#include` directives
    std::string processIncludes(const std::string& input, const std::vector<std::string>& includeDirs) {
        std::regex includeRegex(R"(#include\s*["<](.*?)[">])");
        std::smatch match;
        std::string output = input;
        std::string::const_iterator searchStart(output.cbegin());
    
        while (std::regex_search(searchStart, output.cend(), match, includeRegex)) {
            std::string includeFile = match[1].str();
            std::string fileContent;
            try {
                std::string filePath = findIncludeFile(includeFile, includeDirs);
                fileContent = readFile(filePath);
            }
            catch (const std::exception& e) {
                std::cerr << "Error: " << e.what() << std::endl;
                fileContent = "";
            }
    
            // Calculate the position in the full string for the replacement
            auto matchPos = match.position(0) + (searchStart - output.cbegin());
            output.replace(matchPos, match.length(0), fileContent);
    
            // Adjust the searchStart to continue at the inserted content (to allow nested includes)
            searchStart = output.cbegin() + matchPos;
        }
    
        return output;
    }
    
    int main() {
        // Example input string containing `#include` directives
        std::string input = R"(#include "example.glsl")";
    
        // List of additional include directories
        std::vector<std::string> includeDirs = { "./include", "./shaders" };
    
        try {
            std::string output = processIncludes(input, includeDirs);
            std::cout << "Processed Output:\n" << output << std::endl;
        }
        catch (const std::exception& e) {
            std::cerr << "Error: " << e.what() << std::endl;
        }
    
        return 0;
    }
    

    example.glsl

    #include "common.glsl"
    
    void main()
    {
        FragColor = vertexColor;
    }
    

    common.glsl

    out vec4 FragColor;
    in vec4 vertexColor;
    

    Output:

    out vec4 FragColor;
    in vec4 vertexColor;
    
    void main()
    {
        FragColor = vertexColor;
    }