Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

The Shader Workshop

Open a native OpenGL window, draw a triangle, then move a camera around three exhibits.

Project files 8

Own a window and draw a frame

Context and GL resources, shader checks, triangle and camera modes, and image capture.

330 lines main.cpp
#include <GL/glew.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include "contracts.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <locale>
#include <memory>
#include <numbers>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>

namespace {
class GlfwSession {
public:
    GlfwSession() {
        glfwSetErrorCallback([](int code,const char* message){
            std::fprintf(stderr,"GLFW %d: %s\n",code,message?message:"unknown error");
        });
        if(!glfwInit()) throw std::runtime_error("GLFW initialization failed.");
    }
    ~GlfwSession(){glfwTerminate();}
    GlfwSession(const GlfwSession&)=delete;
    GlfwSession& operator=(const GlfwSession&)=delete;
};
struct WindowDelete { void operator()(GLFWwindow* window)const noexcept {glfwDestroyWindow(window);} };
using Window=std::unique_ptr<GLFWwindow,WindowDelete>;

struct BufferDelete {void operator()(GLuint name)const noexcept {glDeleteBuffers(1,&name);}};
struct VertexArrayDelete {void operator()(GLuint name)const noexcept {glDeleteVertexArrays(1,&name);}};
struct ShaderDelete {void operator()(GLuint name)const noexcept {glDeleteShader(name);}};
struct ProgramDelete {void operator()(GLuint name)const noexcept {glDeleteProgram(name);}};

// This move-only owner borrows a context. It neither moves the context between
// threads nor makes queued GPU work complete. Its containing scope owns that proof.
template<class Delete> class GlObject {
public:
    GlObject(GLuint name,GLFWwindow* context):name_{name},context_{context}{
        if(!name||!context||glfwGetCurrentContext()!=context)
            throw std::runtime_error("GL object creation requires its current context.");
    }
    ~GlObject(){reset();}
    GlObject(const GlObject&)=delete;
    GlObject& operator=(const GlObject&)=delete;
    GlObject(GlObject&& other)noexcept
        :name_{std::exchange(other.name_,0)},context_{other.context_}{}
    GlObject& operator=(GlObject&& other)noexcept {
        if(this!=&other){reset();name_=std::exchange(other.name_,0);context_=other.context_;}
        return *this;
    }
    [[nodiscard]] GLuint get()const noexcept{return name_;}
private:
    void reset()noexcept {
        if(name_){
            // A lifetime contract, asserted in Debug. Keep owners inside the
            // window scope and never switch away before their destruction.
            assert(glfwGetCurrentContext()==context_);
            Delete{}(name_);name_=0;
        }
    }
    GLuint name_{};
    GLFWwindow* context_{};
};
using Shader=GlObject<ShaderDelete>;
using Program=GlObject<ProgramDelete>;
using Buffer=GlObject<BufferDelete>;
using VertexArray=GlObject<VertexArrayDelete>;
static_assert(!std::is_copy_constructible_v<Buffer>);
static_assert(std::is_nothrow_move_constructible_v<Buffer>);

std::string read_shader(const std::filesystem::path& path){
    std::ifstream input{path,std::ios::binary};
    if(!input)throw std::runtime_error("Could not open shader: "+path.string());
    std::array<char,65537> storage{};
    input.read(storage.data(),static_cast<std::streamsize>(storage.size()));
    if(input.bad())throw std::runtime_error("Shader read failed.");
    const auto count=static_cast<std::size_t>(input.gcount());
    if(count==0||count>65536)throw std::runtime_error("Shader must contain 1–65536 bytes.");
    return {storage.data(),count};
}

std::string shader_log(GLuint object,bool program){
    std::array<char,8192> log{};
    GLsizei count{};
    if(program)glGetProgramInfoLog(object,static_cast<GLsizei>(log.size()),&count,log.data());
    else glGetShaderInfoLog(object,static_cast<GLsizei>(log.size()),&count,log.data());
    if(count<0||count>=static_cast<GLsizei>(log.size()))return "No bounded compiler log available.";
    return {log.data(),static_cast<std::size_t>(count)};
}
Shader compile(GLenum kind,std::string_view source,GLFWwindow* context){
    if(source.empty()||source.size()>65536)throw std::runtime_error("Unsupported shader source length.");
    Shader shader{glCreateShader(kind),context}; // Ownership precedes fallible compilation.
    const char* bytes=source.data();
    const GLint length=static_cast<GLint>(source.size());
    glShaderSource(shader.get(),1,&bytes,&length);
    glCompileShader(shader.get());
    GLint success{};glGetShaderiv(shader.get(),GL_COMPILE_STATUS,&success);
    if(success!=GL_TRUE)throw std::runtime_error(std::string{kind==GL_VERTEX_SHADER?"Vertex":"Fragment"}+
                                               " shader compilation failed:\n"+shader_log(shader.get(),false));
    return shader;
}
Program link(const Shader& vertex,const Shader& fragment,GLFWwindow* context){
    Program program{glCreateProgram(),context};
    glAttachShader(program.get(),vertex.get());glAttachShader(program.get(),fragment.get());
    glLinkProgram(program.get());
    GLint success{};glGetProgramiv(program.get(),GL_LINK_STATUS,&success);
    if(success!=GL_TRUE)throw std::runtime_error("Program linking failed:\n"+shader_log(program.get(),true));
    glDetachShader(program.get(),vertex.get());glDetachShader(program.get(),fragment.get());
    return program;
}
void check_gl(const char* operation){
    if(const GLenum error=glGetError();error!=GL_NO_ERROR)
        throw std::runtime_error(std::string{operation}+" failed with GL error "+std::to_string(error));
}

void capture(const std::filesystem::path& path,int width,int height){
    const auto size=observatory::rgb_bytes(width,height);
    if(!size)throw std::runtime_error(std::string{size.error()});
    std::vector<unsigned char> pixels(*size);
    glPixelStorei(GL_PACK_ALIGNMENT,1);
    glReadBuffer(GL_BACK);
    glReadPixels(0,0,width,height,GL_RGB,GL_UNSIGNED_BYTE,pixels.data());
    check_gl("Framebuffer capture");
    std::ofstream output{path,std::ios::binary|std::ios::trunc};
    if(!output)throw std::runtime_error("Could not open capture destination.");
    output.imbue(std::locale::classic());
    output<<"P6\n"<<width<<' '<<height<<"\n255\n";
    const auto row_bytes=static_cast<std::size_t>(width)*3;
    // OpenGL reads bottom row first; PPM writes the top row first.
    for(int y=height-1;y>=0;--y)
        output.write(reinterpret_cast<const char*>(pixels.data()+static_cast<std::size_t>(y)*row_bytes),
                     static_cast<std::streamsize>(row_bytes));
    output.close();
    if(!output)throw std::runtime_error("Capture write failed; output may be incomplete.");
}

struct Vertex {std::array<float,3> position;std::array<float,3> color;};
static_assert(std::is_standard_layout_v<Vertex>);
constexpr std::array<Vertex,3> vertices{{
    {{{-0.72F,-0.55F,0}},{{0.95F,0.24F,0.12F}}},
    {{{ 0.72F,-0.55F,0}},{{0.12F,0.42F,0.92F}}},
    {{{ 0.00F, 0.72F,0}},{{0.98F,0.72F,0.20F}}}
}};
} // namespace

int main(int argc,char* argv[]){
    try {
        bool smoke=false,bad_shader=false,camera_mode=false,resize_smoke=false,reverse_draw=false;
        std::filesystem::path destination;
        std::optional<GLenum> check_stage;
        std::filesystem::path check_source;
        std::optional<std::pair<std::filesystem::path,std::filesystem::path>> check_program;
        for(int i=1;i<argc;++i){
            const std::string_view arg{argv[i]};
            if(arg=="--smoke")smoke=true;
            else if(arg=="--camera")camera_mode=true;
            else if(arg=="--resize-smoke"){smoke=true;camera_mode=true;resize_smoke=true;}
            else if(arg=="--reverse-draw"){camera_mode=true;reverse_draw=true;}
            else if(arg=="--bad-shader"){smoke=true;bad_shader=true;}
            else if(arg=="--capture"&&i+1<argc){destination=argv[++i];smoke=true;}
            else if(arg=="--check-program"&&i+2<argc){
                const std::filesystem::path vertex{argv[++i]},fragment{argv[++i]};
                check_program=std::pair{vertex,fragment};smoke=true;
            }
            else if(arg=="--check-stage"&&i+2<argc){
                const std::string_view stage{argv[++i]};
                if(stage=="vertex")check_stage=GL_VERTEX_SHADER;
                else if(stage=="fragment")check_stage=GL_FRAGMENT_SHADER;
                else {std::cerr<<"Stage must be vertex or fragment.\n";return 1;}
                check_source=argv[++i];smoke=true;
            }
            else {std::cerr<<"Usage: observatory [--camera] [--resize-smoke] [--reverse-draw] [--smoke] [--bad-shader] [--capture output.ppm] [--check-stage vertex|fragment file] [--check-program vertex-file fragment-file]\n";return 1;}
        }
        GlfwSession glfw;
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR,3);
        glfwWindowHint(GLFW_OPENGL_PROFILE,GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT,GLFW_TRUE);
#endif
        glfwWindowHint(GLFW_DEPTH_BITS,24);
        if(smoke)glfwWindowHint(GLFW_VISIBLE,GLFW_FALSE);
        Window window{glfwCreateWindow(880,600,"Chromatic Observatory",nullptr,nullptr)};
        if(!window)throw std::runtime_error("Could not create an OpenGL 3.3 core context.");
        glfwMakeContextCurrent(window.get());
        glewExperimental=GL_TRUE;
        if(const auto status=glewInit();status!=GLEW_OK)
            throw std::runtime_error(reinterpret_cast<const char*>(glewGetErrorString(status)));
        // GLEW can probe unsupported enums during core-context initialization.
        // Drain loader-only status before any application GL work begins.
        for(GLenum error=glGetError();error!=GL_NO_ERROR;error=glGetError()){
            if(error!=GL_INVALID_ENUM)throw std::runtime_error("Unexpected GL status after loader initialization: "+std::to_string(error));
        }
        if(!GLEW_VERSION_3_3)throw std::runtime_error("The loaded driver does not support OpenGL 3.3.");
        std::cout<<"GL_VENDOR: "<<glGetString(GL_VENDOR)<<"\nGL_VERSION: "<<glGetString(GL_VERSION)<<"\nGL_RENDERER: "<<glGetString(GL_RENDERER)<<'\n';
        glfwSwapInterval(1);
        {
            // All GL owners die before window and GLFW. This context stays current.
            if(check_program){
                const auto vertex=compile(GL_VERTEX_SHADER,read_shader(check_program->first),window.get());
                const auto fragment=compile(GL_FRAGMENT_SHADER,read_shader(check_program->second),window.get());
                const auto program=link(vertex,fragment,window.get());
                std::cout<<"Shader program linked; no uniform updates or draw were tested.\n";
                return 0;
            }
            if(check_stage){
                const auto shader=compile(*check_stage,read_shader(check_source),window.get());
                std::cout<<"Shader stage compiled; no program link or draw was tested.\n";
                return 0;
            }
            auto vertex=compile(GL_VERTEX_SHADER,read_shader(std::filesystem::path{OBSERVATORY_SHADER_DIR}/(camera_mode?"camera.vert":"observatory.vert")),window.get());
            const auto fragment_source=bad_shader?std::string{"#version 330 core\nnot valid GLSL"}:
                read_shader(std::filesystem::path{OBSERVATORY_SHADER_DIR}/"observatory.frag");
            auto fragment=compile(GL_FRAGMENT_SHADER,fragment_source,window.get());
            auto program=link(vertex,fragment,window.get());
            GLuint vao_name{},buffer_name{};glGenVertexArrays(1,&vao_name);
            VertexArray vao{vao_name,window.get()};
            glGenBuffers(1,&buffer_name);Buffer buffer{buffer_name,window.get()};
            glBindVertexArray(vao.get());glBindBuffer(GL_ARRAY_BUFFER,buffer.get());
            glBufferData(GL_ARRAY_BUFFER,static_cast<GLsizeiptr>(sizeof(vertices)),vertices.data(),GL_STATIC_DRAW);
            glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,static_cast<GLsizei>(sizeof(Vertex)),
                                  reinterpret_cast<const void*>(offsetof(Vertex,position)));
            glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,static_cast<GLsizei>(sizeof(Vertex)),
                                  reinterpret_cast<const void*>(offsetof(Vertex,color)));
            glEnableVertexAttribArray(0);glEnableVertexAttribArray(1);
            const GLint model_location=glGetUniformLocation(program.get(),"u_model");
            if(model_location<0)throw std::runtime_error("Required u_model interface is absent.");
            const GLint view_location=camera_mode?glGetUniformLocation(program.get(),"u_view"):-1;
            const GLint projection_location=camera_mode?glGetUniformLocation(program.get(),"u_projection"):-1;
            if(camera_mode&&(view_location<0||projection_location<0))
                throw std::runtime_error("Required camera uniform interface is absent.");
            if(camera_mode){
                GLint depth_bits{};
                glGetFramebufferAttachmentParameteriv(GL_FRAMEBUFFER,GL_DEPTH,GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE,&depth_bits);
                check_gl("Default framebuffer depth attachment query");
                if(depth_bits<=0)throw std::runtime_error("Camera mode requires a depth buffer.");
                glEnable(GL_DEPTH_TEST);glDepthFunc(GL_LESS);
            }
            check_gl("Geometry and interface setup");
            float yaw=0,pitch=0,distance=4;
            double previous_time=glfwGetTime(),resize_deadline=0;
            bool resize_requested=false;
            int original_width{},original_height{};
            if(camera_mode&&!smoke)std::cout<<"Arrow keys orbit; W/S zoom; R resets; Escape closes.\n";
            do {
                glfwPollEvents();
                const double now=glfwGetTime();
                const float delta=static_cast<float>(std::clamp(now-previous_time,0.0,0.05));
                previous_time=now;
                if(glfwGetKey(window.get(),GLFW_KEY_ESCAPE)==GLFW_PRESS)glfwSetWindowShouldClose(window.get(),GLFW_TRUE);
                if(glfwWindowShouldClose(window.get()))break;
                int width{},height{};glfwGetFramebufferSize(window.get(),&width,&height);
                if(width<=0||height<=0){
                    if(smoke)throw std::runtime_error("No drawable framebuffer.");
                    glfwWaitEvents();previous_time=glfwGetTime();continue;
                }
                if(resize_requested&&width==original_width&&height==original_height){
                    if(now>resize_deadline)throw std::runtime_error("Framebuffer resize did not arrive within two seconds.");
                    glfwWaitEventsTimeout(0.01);continue;
                }
                // Viewport and projection use the same pixel extent, never the logical window size.
                glViewport(0,0,width,height);
                glClearColor(0.035F,0.055F,0.10F,1);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
                glUseProgram(program.get());glBindVertexArray(vao.get());
                if(camera_mode){
                    if(!smoke&&glfwGetWindowAttrib(window.get(),GLFW_FOCUSED)==GLFW_TRUE){
                        const auto pressed=[&](int key){return glfwGetKey(window.get(),key)==GLFW_PRESS?1.0F:0.0F;};
                        yaw=std::remainder(yaw+(pressed(GLFW_KEY_RIGHT)-pressed(GLFW_KEY_LEFT))*delta,2*std::numbers::pi_v<float>);
                        pitch=std::clamp(pitch+(pressed(GLFW_KEY_UP)-pressed(GLFW_KEY_DOWN))*delta,-1.2F,1.2F);
                        distance=std::clamp(distance+(pressed(GLFW_KEY_S)-pressed(GLFW_KEY_W))*delta*2,1.5F,10.0F);
                        if(pressed(GLFW_KEY_R)>0){yaw=0;pitch=0;distance=4;}
                    }
                    const observatory::Vec3 eye{distance*std::sin(yaw)*std::cos(pitch),distance*std::sin(pitch),
                                                distance*std::cos(yaw)*std::cos(pitch)};
                    const auto view=observatory::look_at(eye,{0,0,0},{0,1,0});
                    const auto projection=observatory::perspective(50,static_cast<float>(width)/static_cast<float>(height),0.1F,30);
                    if(!view||!projection)throw std::runtime_error("Camera parameters violated the supported bounds.");
                    glUniformMatrix4fv(view_location,1,GL_FALSE,view->data.data());
                    glUniformMatrix4fv(projection_location,1,GL_FALSE,projection->data.data());
                    const std::array models{
                        observatory::multiply(observatory::translation({-0.45F,0,-0.5F}),observatory::rotate_z(-0.25F)),
                        observatory::multiply(observatory::translation({0.45F,0,0}),observatory::rotate_z(0.2F)),
                        observatory::translation({0,-0.25F,0.5F})};
                    for(std::size_t draw=0;draw<models.size();++draw){
                        const auto index=reverse_draw?models.size()-1-draw:draw;
                        glUniformMatrix4fv(model_location,1,GL_FALSE,models[index].data.data());
                        glDrawArrays(GL_TRIANGLES,0,3);
                    }
                }else{
                    const auto model=observatory::rotate_z(smoke?0.0F:static_cast<float>(std::fmod(now*0.15,6.283185307)));
                    glUniformMatrix4fv(model_location,1,GL_FALSE,model.data.data());
                    glDrawArrays(GL_TRIANGLES,0,3);
                }
                check_gl("Frame draw");
                if(smoke)std::cout<<"Drawable framebuffer: "<<width<<'x'<<height<<" pixels.\n";
                if(resize_smoke&&!resize_requested){
                    original_width=width;original_height=height;
                    glfwSetWindowSize(window.get(),640,480);
                    resize_requested=true;resize_deadline=glfwGetTime()+2;
                    continue;
                }
                if(resize_smoke){
                    std::array<GLint,4> viewport{};glGetIntegerv(GL_VIEWPORT,viewport.data());
                    if(viewport[2]!=width||viewport[3]!=height||
                       std::abs(static_cast<float>(width)/static_cast<float>(height)-4.0F/3)>0.01F)
                        throw std::runtime_error("Resized viewport or framebuffer aspect did not match the checkpoint.");
                    std::cout<<"Resize checkpoint passed: viewport and projection use the new extent.\n";
                }
                if(!destination.empty())capture(destination,width,height);
                glfwSwapBuffers(window.get());
                if(smoke)break;
            } while(!glfwWindowShouldClose(window.get()));
        }
        std::cout<<"All GL owners released while their context was current.\n";
        return 0;
    } catch(const std::exception& error){std::cerr<<error.what()<<'\n';return 1;}
}

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions