easy-gl › Hello Triangle

Hello Triangle

A complete, annotated, minimal example using SDL3 + easy-gl. Renders a colour-animated triangle using a vertex buffer, VAO, and a shader program.

Vertex shader

const char* vertSrc = R"(#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 vertexColor;
void main() {
    gl_Position = vec4(aPos, 1.0);
    vertexColor = aColor;
})";

Fragment shader

const char* fragSrc = R"(#version 330 core
in vec3 vertexColor;
uniform vec4 uTint;
out vec4 FragColor;
void main() {
    FragColor = vec4(vertexColor, 1.0) * uTint;
})";

Vertex data

constexpr float vertices[] = {
    // position          color
    -0.5f, -0.5f, 0.0f,   1.0f, 0.0f, 0.0f,
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,
     0.0f,  0.5f, 0.0f,   0.0f, 0.0f, 1.0f,
};

Full main()

int main(int, char**)
{
    // ---- SDL3 window + context ----
    SDL_Init(SDL_INIT_VIDEO);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

    SDL_Window*    window  = SDL_CreateWindow("Hello Triangle", 800, 600, SDL_WINDOW_OPENGL);
    SDL_GLContext  glCtx   = SDL_GL_CreateContext(window);
    SDL_GL_MakeCurrent(window, glCtx);
    SDL_GL_SetSwapInterval(1);

    // ---- easy-gl resources in a scope so they are destroyed before the context ----
    {
        easygl::Device device;
        device.initialize(
            reinterpret_cast<easygl::GLGetProcAddressFn>(SDL_GL_GetProcAddress));

        // Print GPU info
        const auto& info = device.capabilities().context_info();
        std::cout << info.vendor << " / " << info.renderer << '\n';

        // Compile and link program in one call
        easygl::Program prog(vertSrc, fragSrc);
        const int tintLoc = prog.uniform_location("uTint");

        // VAO + VBO
        easygl::VertexArray vao;
        easygl::Buffer      vbo;
        vao.create();
        vbo.create();

        vao.bind();
        vbo.bind(easygl::BufferTarget::Array);
        vbo.set_data(easygl::BufferTarget::Array, vertices, sizeof(vertices));

        constexpr std::size_t stride = 6 * sizeof(float);
        vao.set_attribute(easygl::VertexAttribute{0, 3, easygl::DataType::Float, false, stride, 0, true});
        vao.set_attribute(easygl::VertexAttribute{1, 3, easygl::DataType::Float, false, stride, 3 * sizeof(float), true});

        // Set initial viewport
        int w = 0, h = 0;
        SDL_GetWindowSizeInPixels(window, &w, &h);
        device.set_viewport(0, 0, w, h);

        // ---- Render loop ----
        bool running = true;
        while (running) {
            SDL_Event ev;
            while (SDL_PollEvent(&ev)) {
                if (ev.type == SDL_EVENT_QUIT) running = false;
                if (ev.type == SDL_EVENT_KEY_DOWN && ev.key.key == SDLK_ESCAPE) running = false;
                if (ev.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED)
                    device.set_viewport(0, 0, ev.window.data1, ev.window.data2);
            }

            device.set_clear_color(0.2f, 0.3f, 0.3f, 1.0f);
            device.clear(easygl::ClearFlags::Color);

            // Animate tint with time
            const float t     = static_cast<float>(SDL_GetTicks()) / 1000.0f;
            const float green = std::sin(t) * 0.5f + 0.5f;

            prog.use();
            prog.set_uniform(tintLoc, 0.7f, green, 1.0f, 1.0f);

            vao.bind();
            device.draw_arrays(easygl::PrimitiveType::Triangles, 0, 3);

            SDL_GL_SwapWindow(window);
        }
    } // vao, vbo, prog, device — all destroyed here

    SDL_GL_DestroyContext(glCtx);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

Key points