C3 OpenGL

Initializing the project #

c3c init c3_opengl
cd c3_opengl

Fetching GLFW and OpenGL #

c3c vendor-fetch glfw
c3c vendor-fetch opengl

NOTE: Unlike in the raylib example, the GLFW package currently do not include a pre-built static library so you need to ensure that you have GLFW installed on your system.

Setting up GLFW #

Let’s edit main.c3 to get a window and basic input handling.

module c3_opengl;

import std::io;
import std::math;
import glfw;

fn int main()
{
    glfw::init();
    defer glfw::terminate();

    GlfwWindow* window;
    window = glfw::createWindow(800, 600, "C3 OpenGL", null, null);

    glfw::makeContextCurrent(window);
    glfw::swapInterval(1);
    glfw::setKeyCallback(window, &key_callback);

    while (!glfw::windowShouldClose(window))
    {
		gl::clear(gl::GL_COLOR_BUFFER_BIT);
		glfw::swapBuffers(window);
    }

    return 0;
}

fn void key_callback(GlfwWindow* window,
                     int key, int scancode,
                     int action, int mods)
{
  	if (key == glfw::KEY_ESCAPE && action == glfw::PRESS)
  	{
  	    glfw::setWindowShouldClose(window, 1);
  	}
}

Run the project using c3c run, you should get an empty window and pressing Escape should cause it to close.

Initializing OpenGL #

Import opengl

import opengl::gl;

Write a function for initializing OpenGL

fn void init_gl()
{
  	opengl::loadGL((GLLoadFn)&glfw::getProcAddress);
  	char* renderer = gl::getString(gl::GL_RENDERER);
  	char* version = gl::getString(gl::GL_VERSION);
  	io::printfn("Renderer: %s", (ZString)renderer);
  	io::printfn("Version: %s", (ZString)version);
}

Call this function from main and run the project again, if all goes well you should see rendering information as well as your OpenGL version being printed.

Drawing our first quad #

In this article we will work our way towards drawing a cube but let’s start out with a single quad meaning we will have to draw two triangles.

First we define an array of floats where each triplet corresponds to a single vertex. Add the following, either globally or in main.

const float[*] VERTICES = {
    -0.5f, -0.5f, 0.5f,
     0.5f, -0.5f, 0.5f,
     0.5f,  0.5f, 0.5f,
     0.5f,  0.5f, 0.5f,
    -0.5f,  0.5f, 0.5f,
    -0.5f, -0.5f, 0.5f,
};

notice that there are some duplicates, this can be avoided by using an index buffer together with an indexed draw-call but we will skip this for the sake simplicity.

Now we need to upload this vertex data to the GPU. We’ll create a vertex buffer (vbo) and a vertex array (vao) in the main function.

uint vbo, vao;
gl::genVertexArrays(1, &vao);
gl::genBuffers(1, &vbo);

defer {
	gl::deleteBuffers(1, &vbo);
	gl::deleteBuffers(1, &vao);
}

Then we bind the buffer and upload the vertex data.

gl::bindVertexArray(vao);
gl::bindBuffer(gl::GL_ARRAY_BUFFER, vbo);
gl::bufferData(
	target: gl::GL_ARRAY_BUFFER,
	size: $sizeof(VERTICES),
	data: &VERTICES,
	usage: gl::GL_STATIC_DRAW
);

notice in the call to bufferData we used the keyword-argument feature of C3.

Now we need to tell OpenGL how to interpret this data, this is done using vertex attributes, i.e. its layout.

gl::enableVertexAttribArray(0);
gl::vertexAttribPointer(
	index: 0,
	size: 3,
	type: gl::GL_FLOAT,
	normalized: gl::GL_FALSE,
	stride: 3 * float.sizeof,
	pointer: (void*)0
);

The vertex positions is the first and currently only attribute, it has index 0. Each vertex position has a size of 3, i.e. number of elements. Each element has type float and between each vertex position there is a stride of 3 * float.sizeof bytes to the next one. We also specify an offset pointer of zero to the first vertex position.

Finally, we need to add a call to gl::drawArrays in our main loop.

while (!glfw::windowShouldClose(window))
{
	gl::clear(gl::GL_COLOR_BUFFER_BIT);
	gl::drawArrays(gl::GL_TRIANGLES, 0, 6);
	glfw::swapBuffers(window);
}

We should now be able to run our program and see our first quad.

Shaders #

Before adding more quads we should add shaders. Define the following pair of shader sources.

const char* VERTEX_SHADER = `
	#version 410 core
	layout(location = 0) in vec3 apos;

	void main()
	{
  		gl_Position = vec4(apos, 1.0);
	}
`;

const char* FRAGMENT_SHADER = `
	#version 410 core
	out vec4 FragColor;

	void main()
	{
	    FragColor = vec4(1.0, 0.0, 0.0, 1.0);
	}
`;

For the sake of convenience, let’s make a function that will load and compile our shaders. Let’s also create a new module for this, either in the same file or a separate one.

module shader;
import std::io;
import std::math::matrix;
import opengl::gl;

faultdef SHADER_COMPILATION_FAILED;

fn uint? compile(GLenum shadertype, char** src)
{
	int id = gl::createShader(shadertype);
	gl::shaderSource(id, 1, src, null);
	gl::compileShader(id);

	int success;
	gl::getShaderiv(shader, gl::GL_COMPILE_STATUS, &success);

	if (!success)
	{
  		GLsizei len;
  		char[512] infoLog;
  		gl::getShaderInfoLog(id, 512, &len, &infoLog);
  		io::eprintfn("SHADER ERROR: %s", (String)infoLog[:len]);
  		return SHADER_COMPILATION_FAILED?;
	}

	return id;
}

Our vertex and fragment shader will need to be combined into a single shader program. Let’s add a struct that will hold the ids of both shaders and the shader program itself.

struct Shader
{
	uint id;
	uint vertex_id;
	uint fragment_id;
}

and a function for creating the shader.

fn Shader? create(char** vert_src, char** frag_src)
{
    uint vertex_id = compile(gl::GL_VERTEX_SHADER, vert_src)!;
    uint fragment_id = compile(gl::GL_FRAGMENT_SHADER, frag_src)!;
    uint id = gl::createProgram();

    gl::attachShader(id, vertex_id);
    gl::attachShader(id, fragment_id);
    gl::linkProgram(id);

    return {
        .vertex_id = vertex_id,
        .fragment_id = fragment_id,
        .id = id
    };
}

Let’s also create the following methods. We could have used regular functions but I will use this as an opportunity to introduce methods.

fn void Shader.delete(&self)
{
	gl::detachShader(self.id, self.vertex_id);	
	gl::detachShader(self.id, self.fragment_id);	
	gl::deleteShader(self.vertex_id);
	gl::deleteShader(self.fragment_id);
	gl::deleteProgram(self.id);
}

fn void Shader.use(&self)
{
	gl::useProgram(self.id);
}

That’s it for our shader module, let’s try it out. In our main function, add

Shader shader = shader::create(&VERTEX_SHADER, &FRAGMENT_SHADER)!!;
defer shader.delete();
shader.use();

If we run the program we should get the same quad as before but this time it’s red!

Camera #

There’s no point in drawing a cube if we can only see the front of it so let’s implement a camera. Again, let’s make a separate module for this but before doing so I’d like to introduce the notation for vectors in C3. A vector of 3 floats is denoted by

float[<3>]

we will use these a lot so I’m going to create a type alias for them at in our camera module for now.

Let’s start off with the following

module camera::cam;

import std::math;
import std::math::matrix;

alias Vec3 = float[<3>];

struct Camera
{
	float fov;
	float yaw;
	float pitch;
	float speed;
	float sensitivity;
	Vec3 position;
	Vec3 front;
	Vec3 up;
	Vec3 right;
	Vec3 world_up;
}

enum Dir
{
  	FORWARD, BACKWARD, LEFT, RIGHT, UP, DOWN,
}

fn void init(Camera* cam)
{
	cam.fov = (float)math::deg_to_rad(45);
	cam.yaw = -90;
	cam.pitch = 0;
	cam.speed = 0.3;
	cam.sensitivity = 0.2;
	cam.position = { 0, 0, 1 };
	cam.front = { 0, 0, -1 };
	cam.up = { 0, 1, 0 };
	cam.right = { -1, 0, 0 };
	cam.world_up = { 0, 1, 0 };
	update(cam);
}

fn void update(Camera* cam)
{
  	float cam_yaw = (float)math::deg_to_rad(cam.yaw);
  	float cam_pitch = (float)math::deg_to_rad(cam.pitch);

  	cam.front.x = math::cos(cam_yaw) * math::cos(cam_pitch);
  	cam.front.y = math::sin(cam_pitch);
  	cam.front.z = math::sin(cam_yaw) * math::cos(cam_pitch);

  	cam.front.normalize();
	
  	cam.right = cam.front.cross(cam.world_up).normalize();
  	cam.up = cam.right.cross(cam.front).normalize();
}

fn void Camera.move(&self, Dir dir, float dt)
{
	switch (dir)
	{
		case FORWARD: self.position += (self.front * dt);
		case BACKWARD: self.position -= (self.front * dt);
		case LEFT: self.position -= (self.right * dt);
		case RIGHT: self.position += (self.right * dt);
		case UP: self.position += (self.up * dt);
		case DOWN: self.position -= (self.up * dt);
	}
}


fn void perspective(
	float fov, float aspect, float znear, float zfar, Matrix4f* mat)
{
	float tan_half_fov = (float)math::tan(fov * 0.5);

	mat.m[0] = 1 / (aspect * tan_half_fov);
	mat.m[5] = 1 / tan_half_fov;
	mat.m[10] = - (zfar + znear) / (zfar - znear);
	mat.m[11] = -1;
	mat.m[14] = - (2 * zfar * znear) / (zfar - znear);
	mat.m[15] = 0;
}

macro look_at(Vec3 eye, Vec3 target, Vec3 up, Matrix4f* mat)
{
    Vec3 f = (eye - target).normalize(); // forward
    Vec3 r = up.cross(f).normalize();    // right
    Vec3 u = f.cross(r).normalize();     // up

    mat.m = {
    	// rotation                       // translation
    	[0] = r.x, [4] = r.y, [8]  = r.z, [12] = -r.dot(eye),
    	[1] = u.x, [5] = u.y, [9]  = u.z, [13] = -u.dot(eye),
    	[2] = f.x, [6] = f.y, [10] = f.z, [14] = -f.dot(eye),
    	[3] = 0.0, [7] = 0.0, [11] = 0.0, [15] = 1.0
    };
}

fn void view(Camera* cam, Matrix4f* dest)
{
    Vec3 center = cam.position + cam.front;
    look_at(cam.position, center, cam.up, dest);
}

fn void projection(Camera* cam, float aspect, Matrix4f* mat)
{
	return perspective(cam.fov, aspect, 0.1f, 100.1f, mat);
}

and import camera::cam in the main module.

Note that the standard library comes with a look_at and perspective function but they currently return a matrix stored in row-major order but this will likely change.

OpenGL assumes column-major order and we would need to tell it to transpose the matrix if we were to use the standard library implementation.

In the main module we define a global camera variable

Camera cam;

and initialize it in our main function.

cam::init(&cam);

Shader updates #

We are going to modify the vertex shader to apply a view and perspective matrix to every vertex. Our new vertex shader should look like this

const char* VERTEX_SHADER = `
	#version 410 core
	layout(location = 0) in vec3 apos;

	uniform mat4 view;
	uniform mat4 proj;

	void main()
	{
		gl_Position = proj * view * vec4(apos, 1.0);
	}
`;

In the main loop we need to set camera view matrix in our shader before the draw call. Our updated main loop will look like this

while (!glfw::windowShouldClose(window))
{
	Matrix4f view = matrix::IDENTITY4{float};
	cam::view(&cam, &view);
	shader.set_mat4("view", &view); // TODO

	gl::clear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT);
	gl::drawArrays(gl::GL_TRIANGLES, 0, 6);
	glfw::swapBuffers(window);
	glfw::pollEvents();
}

We also need to implement set_mat4 in our shader module

fn void Shader.set_mat4(&self, String uniform, Matrix4f* mat)
{
    gl::uniformMatrix4fv(
        gl::getUniformLocation(self.id, uniform),
        1, gl::GL_FALSE, &mat.m
    );
}

Add a set_vec3 as well, we will need it later

fn void Shader.set_vec3(&self, String uniform, float[<3>] vec)
{
	gl::uniform3f(gl::getUniformLocation(self.id, uniform),
		vec.x, vec.y, vec.z);
}

Now in our main loop we need to set these matrices in our shader, we also need to clear the color and depth buffer. Add the following before the call to drawArrays

Matrix4f view = matrix::IDENTITY4{float};
cam::view(&cam, &view);
shader.set_mat4("view", &view);

Matrix4f proj = matrix::IDENTITY4{float};
cam::proj(&cam, 800/600, &proj);
shader.set_mat4("proj", &proj);

gl::clear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT);

lastly, we need to enable GL_DEPTH_TEST. In the main function, after the call to init_gl add

gl::enable(gl::GL_DEPTH_TEST);

If we run the program now, we should still be able to see our red quad.

Camera movement #

Now let’s hook up keyboard input to our camera movement. Add the following function in the main module.

fn void handle_input(GlfwWindow* window, Camera* cam, float dt)
{
    if (glfw::getKey(window, glfw::KEY_W)) {
    	cam.move(Dir.FORWARD, dt);
    }
    if (glfw::getKey(window, glfw::KEY_S)) {
    	cam.move(Dir.BACKWARD, dt);
    }
    if (glfw::getKey(window, glfw::KEY_A)) {
    	cam.move(Dir.LEFT, dt);
    }
    if (glfw::getKey(window, glfw::KEY_D)) {
    	cam.move(Dir.RIGHT, dt);
    }
    if (glfw::getKey(window, glfw::KEY_E)) {
    	cam.move(Dir.UP, dt);
    }
    if (glfw::getKey(window, glfw::KEY_Q)) {
    	cam.move(Dir.DOWN, dt);
    }
}

to call this function we will need to calculate the time it took for a previous frame to draw (delta_time).

Add these two variables before the main loop

float delta_time = 0.0;
float last_frame_time = 0.0;

then, at the beginning of the main loop we calculate the delta_time and make a call to handle_input. Our main loop should now look like this

while (!glfw::windowShouldClose(window))
{
	float now = (float)glfw::getTime();
	delta_time = now - last_frame_time;
	last_frame_time = now;
	handle_input(window, &cam, delta_time);

	Matrix4f view = matrix::IDENTITY4{float};
	cam::view(&cam, &view);
	shader.set_mat4("view", &view);

	Matrix4f proj = matrix::IDENTITY4{float};
	cam::projection(&cam, 800/600, &proj);
	shader.set_mat4("proj", &proj);

	gl::clear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT);
	gl::drawArrays(gl::GL_TRIANGLES, 0, 6);
	glfw::swapBuffers(window);

	glfw::pollEvents();
}

we should now be able to move around.

Mouse look #

To get the ability to look around we need to add mouse look.

In the camera module

fn void Camera.mouse_look(&self, float xoffset, float yoffset)
{
	self.yaw += xoffset * self.sensitivity;
	self.pitch += yoffset * self.sensitivity;

	if (self.pitch > 89.0f) self.pitch = 89.0f;
	if (self.pitch < -89.0f) self.pitch = -89.0f;

	update(self);
}

In the main module we need to set a couple of glfw callbacks and some global state

float last_x = 0;
float last_y = 0;

fn void cursor_enter_callback(GlfwWindow* window, int entered)
{
	if (entered)
	{
		double x, y;
		glfw::getCursorPos(window, &x, &y);
		last_x = (float)x;
		last_y = (float)y;
	}	
}

fn void mouse_callback(GlfwWindow* window, double x, double y)
{
	float xoffset = (float)x - last_x;
	float yoffset = last_y - (float)y;

	last_x = (float)x;
	last_y = (float)y;

	cam.mouse_look(xoffset, yoffset);
}

The reason for the cursor_enter_callback is that we want to initialize the last_x and last_y variables to something sensible when the cursor enters the window.

Now we need to hook up these callbacks in our main function, we also set our desired inputMode.

glfw::setInputMode(window, glfw::CURSOR, glfw::CURSOR_NORMAL);
glfw::setCursorEnterCallback(window, &cursor_enter_callback);
glfw::setCursorPosCallback(window, &mouse_callback);

If all goes well, we should now be able to look around using the mouse.

Drawing a cube #

We are going to need a few more vertices, 36 of them to be exact.


const float[*] VERTICES = {
    -0.5f, -0.5f, -0.5f,
     0.5f, -0.5f, -0.5f,
     0.5f,  0.5f, -0.5f,
     0.5f,  0.5f, -0.5f,
    -0.5f,  0.5f, -0.5f,
    -0.5f, -0.5f, -0.5f,

    -0.5f, -0.5f,  0.5f,
     0.5f, -0.5f,  0.5f,
     0.5f,  0.5f,  0.5f,
     0.5f,  0.5f,  0.5f,
    -0.5f,  0.5f,  0.5f,
    -0.5f, -0.5f,  0.5f,

    -0.5f,  0.5f,  0.5f,
    -0.5f,  0.5f, -0.5f,
    -0.5f, -0.5f, -0.5f,
    -0.5f, -0.5f, -0.5f,
    -0.5f, -0.5f,  0.5f,
    -0.5f,  0.5f,  0.5f,

     0.5f,  0.5f,  0.5f,
     0.5f,  0.5f, -0.5f,
     0.5f, -0.5f, -0.5f,
     0.5f, -0.5f, -0.5f,
     0.5f, -0.5f,  0.5f,
     0.5f,  0.5f,  0.5f,

    -0.5f, -0.5f, -0.5f,
     0.5f, -0.5f, -0.5f,
     0.5f, -0.5f,  0.5f,
     0.5f, -0.5f,  0.5f,
    -0.5f, -0.5f,  0.5f,
    -0.5f, -0.5f, -0.5f,

    -0.5f,  0.5f, -0.5f,
     0.5f,  0.5f, -0.5f,
     0.5f,  0.5f,  0.5f,
     0.5f,  0.5f,  0.5f,
    -0.5f,  0.5f,  0.5f,
    -0.5f,  0.5f, -0.5f,
};

As mentioned earlier, for the sake of simplicity we will skip indexed drawing which would allow us to reuse vertices.

Now we simply need to update our call to drawArrays like so

gl::drawArrays(gl::GL_TRIANGLES, 0, 36);

Lighting #

Our cube is looking a bit flat, our final task will be to add some basic lighting.

Normal data #

First we need add normal vector data to our vertices.

const float[*] VERTICES = {
	// Position           // Normal
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f, -1.0f,
     0.5f, -0.5f, -0.5f,  0.0f, 0.0f, -1.0f,
     0.5f,  0.5f, -0.5f,  0.0f, 0.0f, -1.0f,
     0.5f,  0.5f, -0.5f,  0.0f, 0.0f, -1.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 0.0f, -1.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, 0.0f, -1.0f,

    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f, 1.0f,
     0.5f, -0.5f,  0.5f,  0.0f, 0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f, 0.0f, 1.0f,
     0.5f,  0.5f,  0.5f,  0.0f, 0.0f, 1.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 0.0f, 1.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, 0.0f, 1.0f,

    -0.5f,  0.5f,  0.5f,  -1.0f, 0.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  -1.0f, 0.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  -1.0f, 0.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  -1.0f, 0.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,  -1.0f, 0.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  -1.0f, 0.0f, 0.0f,

     0.5f,  0.5f,  0.5f,  1.0f, 0.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  1.0f, 0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 0.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  1.0f, 0.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  1.0f, 0.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  1.0f, 0.0f, 0.0f,

    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f, 0.0f,
     0.5f, -0.5f, -0.5f,  0.0f, -1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f, 0.0f,
     0.5f, -0.5f,  0.5f,  0.0f, -1.0f, 0.0f,
    -0.5f, -0.5f,  0.5f,  0.0f, -1.0f, 0.0f,
    -0.5f, -0.5f, -0.5f,  0.0f, -1.0f, 0.0f,

    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f, 0.0f,
     0.5f,  0.5f, -0.5f,  0.0f, 1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  0.0f, 1.0f, 0.0f,
     0.5f,  0.5f,  0.5f,  0.0f, 1.0f, 0.0f,
    -0.5f,  0.5f,  0.5f,  0.0f, 1.0f, 0.0f,
    -0.5f,  0.5f, -0.5f,  0.0f, 1.0f, 0.0f,
};

Updated shaders #

Next, we update the code for our vertex and fragment shader.

const char* VERTEX_SHADER = `
	#version 410 core

	layout(location = 0) in vec3 apos;
	layout(location = 1) in vec3 anormal;

	out vec3 FragPos;
	out vec3 Normal;

	uniform mat4 view;
	uniform mat4 proj;
	uniform mat4 model;

	void main()
	{
		FragPos = vec3(model * vec4(apos, 1.0));
		Normal = mat3(transpose(inverse(model))) * anormal;

		gl_Position = proj * view * model * vec4(FragPos, 1.0);
	}
`;

const char* FRAGMENT_SHADER = `
	#version 410 core
	out vec4 FragColor;

	in vec3 Normal;
	in vec3 FragPos;
	
	uniform vec3 lightPos;
	uniform vec3 lightColor;
	uniform vec3 objectColor;

	void main()
	{
	    // ambient
	    float ambientStrength = 0.1;
	    vec3 ambient = ambientStrength * lightColor;

	    // diffuse
	    vec3 norm = normalize(Normal);
	    vec3 lightDir = normalize(lightPos - FragPos);
	    float diff = max(+(dot(norm, lightDir)), 0.0);
	    vec3 diffuse = diff * lightColor;

	    vec3 result = (ambient + diffuse) * objectColor;
	    FragColor = vec4(result, 1.0);
	}
`;

Notice the addition of a second layout in the vertex shader, just like before we need to tell OpenGL how to interpret this data. Our previous layout has to change as well because the stride just became larger.

We modify our previous layout

gl::enableVertexAttribArray(0);
gl::vertexAttribPointer(
	index: 0,
	size: 3,
	type: gl::GL_FLOAT,
	normalized: gl::GL_FALSE,
	stride: 6 * float.sizeof,
	pointer: (void*)0
);

and enable a second one

gl::enableVertexAttribArray(1);
gl::vertexAttribPointer(
	index: 1,
	size: 3,
	type: gl::GL_FLOAT,
	normalized: gl::GL_FALSE,
	stride: 6 * float.sizeof,
	pointer: (void*)(3*float.sizeof)
);

Updated main loop #

In our main loop we now need to set our new uniform shader variables

shader.set_vec3("lightColor", {1 ,1 ,1});
shader.set_vec3("lightPos", {17, 10, 17});
shader.set_vec3("objectColor", {1, 0, 0});

In our new vertex shader we also added a uniform for a model matrix, we will set it to be the identity matrix for now.

Matrix4f model = matrix::IDENTITY4{float};
shader.set_mat4("model", &model);

Our new and final main loop should now look something like this

while (!glfw::windowShouldClose(window))
{
	float now = (float)glfw::getTime();
	delta_time = now - last_frame_time;
	last_frame_time = now;

	handle_input(window, &cam, delta_time);

	shader.set_vec3("lightColor", {1 ,1 ,1});
	shader.set_vec3("lightPos", {17, 10, 17});

	Matrix4f view = matrix::IDENTITY4{float};
	cam::view(&cam, &view);
	shader.set_mat4("view", &view);

	Matrix4f proj = matrix::IDENTITY4{float};
	cam::projection(&cam, 800/600, &proj);
	shader.set_mat4("proj", &proj);

	Matrix4f model = matrix::IDENTITY4{float};
	shader.set_vec3("objectColor", {1, 0, 0});
	shader.set_mat4("model", &model);

	gl::clear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT);
	gl::drawArrays(gl::GL_TRIANGLES, 0, 36);
	glfw::swapBuffers(window);

	glfw::pollEvents();
}

Finally! #

Full source