I'm trying to do some 3D graphics using OpenGL, with cglm for math.
I was following a guide that lead me to multiplying my vertex position by 3 matrices: model, view and projection.
Since the tutorial wasn't in the same language as me, I couldn't follow it one-by-one, so I did the projection matrix in the way I could find, with cglm
Multiplying the vertex by those matrices gave me a black screen (object not rendered), and I noticed that it just happened because of the projection matrix.
I checked my matrix values and they matched with one projection matrix I've asked ChatGPT, but didn't work, so I done one by myself instead of using the cglm one, and it worked (with some distortion, but worked).
I want to know why the cglm one failed, because I'd rather using theirs.
Here is a important code snippet (C):
mat4 model, view, proj;
glm_mat4_identity(model);
glm_mat4_identity(proj);
glm_rotate(model, PI4, (vec3) { 1, 0, 0 });
// glm_perspective(cam.fov, (f32) cam.width / cam.height, cam.near, cam.far, proj);
proj[0][0] = PI4;
proj[1][1] = PI4;
proj[2][2] = (cam.far + cam.near) / (cam.near - cam.far);
proj[2][3] = -1;
proj[3][2] = 2 * cam.far * cam.near / (cam.near - cam.far);
glUniformMatrix4fv(UNI(shader_program, "MODEL"), 1, GL_FALSE, (const f32*) { model[0] });
glUniformMatrix4fv(UNI(shader_program, "PROJ"), 1, GL_FALSE, (const f32*) { proj[0] });
while (!glfwWindowShouldClose(canvas.window)) {
glm_mat4_identity(view);
glm_translate(view, (vec3) { -cam.pos[0], -cam.pos[1], -cam.pos[2] });
glUniformMatrix4fv(UNI(shader_program, "VIEW"), 1, GL_FALSE, (const f32*) { view[0] });
canvas_clear((RGBA) { 0, 0, 0, 1 });
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(canvas.window);
glfwPollEvents();
handle_inputs(canvas.window);
}
The full code (C):
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cglm/cglm.h>
#include <stdio.h>
#include "shader.h"
#include "misc.h"
#define ASSERT(x, str) if (!(x)) {printf(str); exit(1);}
#define UNI(shader, name) (glGetUniformLocation(shader, name))
#define PI 3.14159
#define PI2 PI / 2
#define PI4 PI / 4
#define TAU PI * 2
#define VERTEX_SHADER "shd/vertex.glsl"
#define FRAGMENT_SHADER "shd/fragment.glsl"
#define TEXTURE_0 "img/bills.ppm"
#define TEXTURE_1 "img/ilu.ppm"
#define WIDTH 800
#define HEIGHT 800
#define MOVEMENT_SPEED 0.01
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef int8_t i8;
typedef int16_t i16;
typedef int32_t i32;
typedef float f32;
typedef double f64;
// --- Type
typedef struct { f32 R, G, B, A; } RGBA;
typedef struct {
i16 width, height;
f32 fov, near, far;
f32 pos[3];
f32 ang[2];
} Camera;
typedef struct {
GLFWwindow* window;
i16 width, height;
} Canvas;
u32 canvas_create_texture(GLenum unit, char path[], u32 shader, char uniform_name[], i32 uniform_value);
void canvas_config_texture(GLenum wrap_s, GLenum wrap_t, GLenum min_filter, GLenum mag_filter);
void key_callback(GLFWwindow* window, i32 key, i32 scancode, i32 action, i32 mods);
void handle_inputs(GLFWwindow* window);
void canvas_init(Canvas* canvas);
void canvas_clear(RGBA color);
u32 canvas_create_VBO();
u32 canvas_create_VAO();
u32 canvas_create_EBO();
// --- Setup
u8 wireframe_mode;
Canvas canvas = { NULL, WIDTH, HEIGHT };
Camera cam = { WIDTH, HEIGHT, PI4, 0.1, 100, { 0, 0, 0 }, { 0, 0 } };
f32 vertices[] = {
+0.00, +0.45, 0, 0.5, 0.0,
-0.50, -0.50, 0, 1.0, 1.0,
+0.50, -0.50, 0, 0.0, 1.0,
};
// --- Main
i8 main() {
canvas_init(&canvas);
glfwSetKeyCallback(canvas.window, key_callback);
u32 VBO = canvas_create_VBO();
u32 VAO = canvas_create_VAO();
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(f32), (void*) 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(f32), (void*) (sizeof(f32) * 3));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
u32 shader_program = shader_create_program(VERTEX_SHADER, FRAGMENT_SHADER);
canvas_config_texture(GL_MIRRORED_REPEAT, GL_MIRRORED_REPEAT, GL_NEAREST, GL_NEAREST);
canvas_create_texture(GL_TEXTURE0, TEXTURE_0, shader_program, "TEXTURE_0", 0);
canvas_create_texture(GL_TEXTURE1, TEXTURE_1, shader_program, "TEXTURE_1", 1);
mat4 model, view, proj;
glm_mat4_identity(model);
glm_mat4_identity(proj);
glm_rotate(model, PI4, (vec3) { 1, 0, 0 });
// glm_perspective(cam.fov, (f32) cam.width / cam.height, cam.near, cam.far, proj);
proj[0][0] = PI4;
proj[1][1] = PI4;
proj[2][2] = (cam.far + cam.near) / (cam.near - cam.far);
proj[2][3] = -1;
proj[3][2] = 2 * cam.far * cam.near / (cam.near - cam.far);
glUniformMatrix4fv(UNI(shader_program, "MODEL"), 1, GL_FALSE, (const f32*) { model[0] });
glUniformMatrix4fv(UNI(shader_program, "PROJ"), 1, GL_FALSE, (const f32*) { proj[0] });
while (!glfwWindowShouldClose(canvas.window)) {
glm_mat4_identity(view);
glm_translate(view, (vec3) { -cam.pos[0], -cam.pos[1], -cam.pos[2] });
glUniformMatrix4fv(UNI(shader_program, "VIEW"), 1, GL_FALSE, (const f32*) { view[0] });
canvas_clear((RGBA) { 0, 0, 0, 1 });
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(canvas.window);
glfwPollEvents();
handle_inputs(canvas.window);
}
glfwTerminate();
return 0;
}
// --- Function
void canvas_init(Canvas* canvas) {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
canvas->window = glfwCreateWindow(canvas->width, canvas->height, "Coordinate System", NULL, NULL);
ASSERT(canvas->window, "Failed creating a window");
glfwMakeContextCurrent(canvas->window);
ASSERT(gladLoadGLLoader((GLADloadproc) glfwGetProcAddress), "Failed loading glad");
glViewport(0, 0, WIDTH, HEIGHT);
}
void canvas_clear(RGBA color) {
glClearColor(color.R, color.G, color.B, color.A);
glClear(GL_COLOR_BUFFER_BIT);
}
void canvas_config_texture(GLenum wrap_s, GLenum wrap_t, GLenum min_filter, GLenum mag_filter) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap_s);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap_t);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
}
u32 canvas_create_texture(GLenum unit, char path[], u32 shader, char uniform_name[], i32 uniform_value) {
u32 texture_ID;
glGenTextures(1, &texture_ID);
glActiveTexture(unit);
glBindTexture(GL_TEXTURE_2D, texture_ID);
u16 width, height;
get_image_size(path, &width, &height);
f32* image_buffer = malloc(sizeof(f32) * width * height * 3);
load_image(path, image_buffer, width * height);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_FLOAT, image_buffer);
glGenerateMipmap(GL_TEXTURE_2D);
glUniform1i(UNI(shader, uniform_name), uniform_value);
free(image_buffer);
return texture_ID;
}
void key_callback(GLFWwindow* window, i32 key, i32 scancode, i32 action, i32 mods) {
if (action != GLFW_PRESS) return;
switch (key) {
case GLFW_KEY_ESCAPE:
glfwSetWindowShouldClose(canvas.window, 1);
break;
case GLFW_KEY_M:
if (wireframe_mode) glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
else glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
wireframe_mode = 1 - wireframe_mode;
}
}
void handle_inputs(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
cam.pos[2] += MOVEMENT_SPEED;
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
cam.pos[2] -= MOVEMENT_SPEED;
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
cam.pos[0] -= MOVEMENT_SPEED;
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
cam.pos[0] += MOVEMENT_SPEED;
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS)
cam.pos[1] += MOVEMENT_SPEED;
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS)
cam.pos[1] -= MOVEMENT_SPEED;
}
u32 canvas_create_VBO() {
u32 VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
return VBO;
}
u32 canvas_create_VAO() {
u32 VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
return VAO;
}
u32 canvas_create_EBO() {
u32 EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
return EBO;
}
My vertex shader (GLSL):
# version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTex;
uniform mat4 MODEL;
uniform mat4 VIEW;
uniform mat4 PROJ;
out vec2 _tex;
void main() {
_tex = aTex;
gl_Position = PROJ * VIEW * MODEL * vec4(aPos, 1);
}
I also printed the matrices:
Mine
0.785398 0.000000 0.000000 0.000000
0.000000 0.785398 0.000000 0.000000
0.000000 0.000000 -1.002002 -1.000000
0.000000 0.000000 -0.200200 1.000000
CGLM
2.414216 0.000000 0.000000 0.000000
0.000000 2.414216 0.000000 0.000000
0.000000 0.000000 -1.002002 -1.000000
0.000000 0.000000 -0.200200 0.000000
I also noticed now, that mine was wrong, because it wasn't supposed to have that last value as 1, it was happening because I was doing an identity matrix before, when I removed that 1, it stopped working
The calculation for the perspective projection matrix is wrong. The symmetrically perspective projection matrix is:
column 0: 1/(ta*a) 0 0 0
column 1: 0 1/ta 0 0
column 2: 0 0 -(f+n)/(f-n) -1
column 3: 0 0 -2*f*n/(f-n) 0
where a = w / h
and ta = tan(fov_y / 2)
For your code that means:
float aspect = (float)window_width / (float)wondow_height;
proj[0][0] = 1 / (tan(PI4 / 2) * aspect);
proj[1][1] = 1 / tan(PI4 / 2);
proj[2][2] = (cam.far + cam.near) / (cam.near - cam.far);
proj[2][3] = -1;
proj[3][2] = 2 * cam.far * cam.near / (cam.near - cam.far);
proj[3][3] = 0;
Note, glm_mat4_identity
initializes a matrix with the identity matrix. So proj[3][3]
needs to be set 0.