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.
#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
