Search code examples
c++openglrenderingglfwimgui

How do I reset the GLFW Fragment / Load a different shader during runtime?


I'm writing a shadertoy-like tool for the desktop. It uses GLFW (OpenGL), Glad, and ImGui to render out images.

I've gotten to a point now where I fixed all my previous issues, but I'm now stuck on one particular problem that's been plaguing me the past few days.

I want to be able to hot-reload shaders. Right now it takes a simple mandelbulb raymarcher GLSL fragment, draws it, and also draws a little ImGui panel with a "reload shader" button.

Ive been trying to figure out how to unlink the current shaderprogram, and load it again with a new shader, but Ive come up with nothing. The code doesnt have any errors, but it definitely doesnt work. When I click the button the OpenGL Viewport just goes black.

I've tried looking online but there were essentially no solid solutions for this, most of the questions I already saw were never actually answered.

This is my current code for resetting the shader (and grabbing it from the files again)

        if (ImGui::Button("Reload Shader", ImVec2(0, 0))) {
            glDeleteVertexArrays(1, &VAO);
            glDeleteBuffers(1, &VBO);
            glDeleteProgram(shaderProgram);
            gladLoadGL();
            glViewport(0, 0, width, height);

            vertexShader = glCreateShader(GL_VERTEX_SHADER);
            fragmentShader = loadShaderFromFile("assets\\round.glsl", GL_FRAGMENT_SHADER);

            shaderProgram = glCreateProgram();
            glAttachShader(shaderProgram, vertexShader);
            glAttachShader(shaderProgram, fragmentShader);
            glLinkProgram(shaderProgram);
            glDeleteShader(vertexShader);
            glDeleteShader(fragmentShader);

            GLuint VAO, VBO;
            glGenVertexArrays(1, &VAO);
            glGenBuffers(1, &VBO);
            glBindVertexArray(VAO);
            glBindBuffer(GL_ARRAY_BUFFER, VBO);
            glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
            glEnableVertexAttribArray(0);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);

            glUseProgram(shaderProgram);
            GLint resUniform = glGetUniformLocation(shaderProgram, "iResolution");
            glUniform3f(resUniform, width, height, width / height);
            double mxpos, mypos;
            glfwGetCursorPos(window, &mxpos, &mypos);
            GLint mouseUniform = glGetUniformLocation(shaderProgram, "iMouse");
            glUniform4f(mouseUniform, mxpos, mypos, mxpos, mypos);
            GLint timeUniform = glGetUniformLocation(shaderProgram, "iTime");
            glUniform1f(timeUniform, currentTime);
            glBindVertexArray(VAO);
            glDrawArrays(GL_TRIANGLES, 0, 3);

            glfwSwapBuffers(window);
            glfwPollEvents();
        }

This is the current github commit with all the code / the entire program, incase its needed:

https://github.com/ArctanStudios/OpenGL-Sandbox/tree/e7ba32310b898d1a60eae89f8969179600f5d391

The actual Main.cpp file with the rest of my GLFW code:

https://github.com/ArctanStudios/OpenGL-Sandbox/blob/e7ba32310b898d1a60eae89f8969179600f5d391/Main.cpp


Solution

  • So after fiddling around a while, I actually figured it out.

    Turns out I was doing way more than was required.

    All I needed to do was detach the fragment at the start, then delete everything, then re-attach a new fragment, link the program again, then detach and delete it again.

    Now I can reload my shaders during runtime.

    Code I used during initialization:

        GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
        glCompileShader(vertexShader);
    
        GLuint fragmentShader = loadShaderFromFile("assets\\round.glsl", GL_FRAGMENT_SHADER);
    
        GLuint shaderProgram = glCreateProgram();
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);
        glDetachShader(shaderProgram, fragmentShader);
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
    

    What this does is just grab the frag + vert, attach them both, cache / link the program to the renderer, then detach the frag, delete the vertex + frag to save memory, and its ready to be overwritten at any time.

    This is the code I used inside of my reload button:

            if (ImGui::Button("Reload Shader", ImVec2(0, 0))) {
                glDetachShader(shaderProgram, fragmentShader);
                fragmentShader = loadShaderFromFile("assets\\round.glsl", GL_FRAGMENT_SHADER);
                glAttachShader(shaderProgram, fragmentShader);
                glLinkProgram(shaderProgram);
                glDetachShader(shaderProgram, fragmentShader);
                glDeleteShader(fragmentShader);
            }
    

    What this does is grab the current shader program, with no fragment, and the already existing vertex still cached on it.

    Then it attaches the new fragment, links / caches the program to the renderer again, then repeats the process from the initial one. it detaches the fragment, deletes it to save memory, then its ready to be overwritten again.

    You can also use this with the vertex shader, just detach and delete it, same as the fragment. I decided not to since the aim of my program is to render 2D shaders onto a plane, similarly to shadertoy