C/C++ 中 OpenGL 着色器的简单框架

2021-12-19 00:00:00 opengl c c++

我只是想在平面图像上尝试一些着色器.事实证明,编写一个 C 程序,它只是将图片作为纹理并应用,比如说高斯模糊,作为它的片段着色器并不是那么容易:你必须初始化 OpenGL,就像 100 行代码,然后了解 GLBuffers 等.此外,要与窗口系统进行通信,必须使用另一种框架 GLUT.

事实证明,Nvidia 的 Fx composer 很适合与着色器一起玩.但我仍然想要一个简单的 C 或 C++ 程序,它只是将给定的片段着色器应用于图像并显示结果.有人有例子或有框架吗?

解决方案

首先,我会避免使用 glut――它有问题,大约十年没有更新,而且它的设计不太合适非常适合当今大多数人的需求(例如,虽然您可以将其用于动画,但它实际上主要用于生成静态显示).我在 之前的答案.

这(大部分)让代码去编译、链接和使用着色器.为此,我编写了一个我觉得很方便的小类:

class shader_prog {GLuint vertex_shader、fragment_shader、prog;模板 <int N>GLuint compile(GLuint type, char const *(&source)[N]) {GLuint 着色器 = glCreateShader(type);glShaderSource(着色器,N,源,NULL);glCompileShader(着色器);GLint 编译;glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);如果(!编译){GLint 长度;glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);std::string log(length, ' ');glGetShaderInfoLog(shader, length, &length, &log[0]);抛出 std::logic_error(log);返回假;}返回着色器;}上市:模板<int N,int M>shader_prog(GLchar const *(&v_source)[N], GLchar const *(&f_source)[M]) {vertex_shader = compile(GL_VERTEX_SHADER, v_source);fragment_shader = compile(GL_FRAGMENT_SHADER, f_source);prog = glCreateProgram();glAttachShader(prog, vertex_shader);glAttachShader(prog, fragment_shader);glLinkProgram(prog);}运算符 GLuint() { 返回程序;}void operator()() { glUseProgram(prog);}~shader_prog() {glDeleteProgram(prog);glDeleteShader(vertex_shader);glDeleteShader(fragment_shader);}};

对于一个简单的演示,几个直通"着色器(只是模仿固定功能的管道):

const GLchar *vertex_shader[] = {"void main(void) {
"," gl_Position = ftransform();
"," gl_FrontColor = gl_Color;
",}"};const GLchar *color_shader[] = {"void main() {
"," gl_FragColor = gl_Color;
",}"};

您会使用以下内容:

void draw() {//编译并链接指定的着色器:静态 shader_prog prog(vertex_shader, color_shader);//使用编译的着色器:编();//画一些东西:glBegin(GL_TRIANGLES);glColor3f(0.0f, 0.0f, 1.0f);glVertex3f(-1.0f, 0.0f, -1.0f);glColor3f(0.0f, 1.0f, 0.0f);glVertex3f(1.0f, 0.0f, -1.0f);glColor3f(1.0f, 0.0f, 0.0f);glVertex3d(0.0, -1.0, -1.0);glEnd();}

例如,如果您要在绘制场景的过程中使用多个不同的片段着色器,只需为每个着色器定义一个静态对象,然后执行 prog1();, prog2(); 等,只是在绘制要使用每个着色器着色的对象之前.例如,

void draw() {static shader_prog wall_shader("wall_vertex", "wall_frag");static shader_prog skin_shader("skin_vertex", "skin_frag");墙着色器();draw_walls();skin_shader();draw_skin();}

正如@rotoglup 非常正确地指出的那样,static 变量的这种使用将破坏延迟到 OpenGL 上下文被破坏之后,因此当析构函数尝试使用 glDeleteProgram/glDeleteShader,结果是不可预测的(充其量).

虽然这在演示程序中是可以原谅的,但在实际使用中却是绝对不可取的.同时,您通常不希望每次输入使用它们的函数时都重新编译着色器.

为了避免这两个问题,您通常希望将着色器对象创建为类实例的成员,该类实例的生命周期反过来又与要着色的对象的生命周期相关联:

class some_character_type {shader_prog skin_shader;上市://...};

这将在您创建该类型的角色时编译/链接一次着色器程序,并在您销毁该角色时销毁它.

当然,在少数情况下,这也不是完全可取的.举个例子,考虑一下像 Galaga 或 Centipede 这样的古老的杀死很多目标"游戏的 3D 版本.对于此类游戏,您需要相对较快地创建和销毁大量基本相同的目标.给定大量基本相同的目标,您可能希望使用类似 shared_ptr 的东西来创建着色器的单个实例,该实例在特定目标类型的所有实例之间共享.鉴于您多次重复使用相同的目标类型,您可能想要更进一步,以便在整个游戏中保持相同的着色器,而不仅仅是在显示特定类型的目标时.

无论如何,我们在这里有点偏离轨道.关键是编译和链接着色器是一个相当的过程,因此您通常希望管理它们的生命周期以避免创建和销毁它们的次数比真正需要的次数要多得多(尽管这并不是说它是在游戏开始时将它们全部创建并在最后销毁它们至关重要).

I just wanted to try out some shaders on a flat image. Turns out that writing a C program, which just takes a picture as a texture and applies, let's say a gaussian blur, as a fragment shader on it is not that easy: You have to initialize OpenGL which are like 100 lines of code, then understanding the GLBuffers, etc.. Also to communicate with the windowing system one has to use GLUT which is another framework..

Turns out that Nvidia's Fx composer is nice to play with shaders.. But I still would like to have a simple C or C++ program which just applies a given fragment shader to an image and displays the result. Does anybody have an example or is there a framework?

解决方案

First of all, I'd avoid using glut -- it's buggy, hasn't been updated in roughly a decade, and its design doesn't really fit very well with what most people want today (e.g., though you can use it for animations, it's really intended primarily to produce a static display). I pointed out a number of alternatives to glut in a previous answer.

That (mostly) leaves the code to compile, link, and use shaders. I've written a small class I find handy for this purpose:

class shader_prog {
    GLuint vertex_shader, fragment_shader, prog;

    template <int N>
    GLuint compile(GLuint type, char const *(&source)[N]) {
        GLuint shader = glCreateShader(type);
        glShaderSource(shader, N, source, NULL);
        glCompileShader(shader);
        GLint compiled;
        glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
        if (!compiled) {
            GLint length;
            glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
            std::string log(length, ' ');
            glGetShaderInfoLog(shader, length, &length, &log[0]);
            throw std::logic_error(log);
            return false;
        }
        return shader;
    }
public:
    template <int N, int M>
    shader_prog(GLchar const *(&v_source)[N], GLchar const *(&f_source)[M]) {
        vertex_shader = compile(GL_VERTEX_SHADER, v_source);
        fragment_shader = compile(GL_FRAGMENT_SHADER, f_source);
        prog = glCreateProgram();
        glAttachShader(prog, vertex_shader);
        glAttachShader(prog, fragment_shader);
        glLinkProgram(prog);
    }

    operator GLuint() { return prog; }
    void operator()() { glUseProgram(prog); }

    ~shader_prog() {
        glDeleteProgram(prog);
        glDeleteShader(vertex_shader);
        glDeleteShader(fragment_shader);
    }
};

For a simple demo, a couple of "pass-through" shaders (just imitate the fixed-functionality pipeline):

const GLchar *vertex_shader[] = {
    "void main(void) {
",
    "    gl_Position = ftransform();
",
    "    gl_FrontColor = gl_Color;
",
    "}"
};

const GLchar *color_shader[] = {
    "void main() {
",
    "    gl_FragColor = gl_Color;
",
    "}"
};

Which you'd use something like:

void draw() { 
    // compile and link the specified shaders:
    static shader_prog prog(vertex_shader, color_shader);

    // Use the compiled shaders:    
    prog(); 

    // Draw something:
    glBegin(GL_TRIANGLES);
        glColor3f(0.0f, 0.0f, 1.0f);
        glVertex3f(-1.0f, 0.0f, -1.0f);
        glColor3f(0.0f, 1.0f, 0.0f);
        glVertex3f(1.0f, 0.0f, -1.0f);
        glColor3f(1.0f, 0.0f, 0.0f);
        glVertex3d(0.0, -1.0, -1.0);
    glEnd();
}

If you're going to use, for example, a number of different fragment shaders in the course of drawing your scene, you simply define a static object for each, then execute prog1();, prog2();, etc., just prior drawing the objects you want shaded with each shader. E.g.,

void draw() { 
    static shader_prog wall_shader("wall_vertex", "wall_frag");
    static shader_prog skin_shader("skin_vertex", "skin_frag");

    wall_shader();
    draw_walls();

    skin_shader();
    draw_skin();
}

Edit: As @rotoglup quite correctly points out, this use of static variables delays destruction until after the OpenGL context has been destroyed, so when the destructors attempt to use glDeleteProgram/glDeleteShader, results are unpredictable (at best).

While this may be excusable in a demo program, it's decidedly undesirable in real use. At the same time, you generally do not want to re-compile your shader(s) every time you enter the function(s) that use them.

To avoid both problems, you generally want to create your shader objects as members of a class instance whose lifetime is, in turn, tied to the lifetime of whatever it's going to shade:

class some_character_type { 
    shader_prog skin_shader;
public:
    // ...
};

This will compile/link the shader program once when you create a character of that type, and destroy it when you destroy that character.

Of course, in a few cases, this isn't exactly desirable either. Just for example, consider a 3D version of the ancient "kill lots of targets" games like Galaga or Centipede. For games like this, you're creating and destroying lots of essentially identical targets relatively quickly. Given a large number of essentially identical targets, you probably want to use something like a shared_ptr<shader_prog> to create a single instance of the shader that's shared between all the instances of a particular target type. Given that you re-use the same target types many times, you may want to go a bit further even than that, so you maintain the same shaders through the entire game, not just when a particular type of target is being shown.

In any case, we're getting a bit off-track here. The point is that compiling and linking shaders is a fairly expensive process, so you normally want to manage their lifetime to avoid creating and destroying them a lot more often than truly necessary (though that's not to say that it's critical to create them all at the beginning of the game and only destroy them at the end, either).

相关文章