Initial commit

This commit is contained in:
2026-09-27 01:33:35 +02:00
commit 3ac329a21b
1185 changed files with 543634 additions and 0 deletions

63
src/camera.cpp Normal file
View File

@@ -0,0 +1,63 @@
#include <algorithm>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "camera.hpp"
namespace Fan
{
glm::mat4 Camera::get_view_matrix() const
{
return glm::lookAt(position, position + get_front(), glm::vec3(0.0f, 1.0f, 0.0f));
}
glm::mat4 Camera::get_projection_matrix(int width, int height) const
{
float aspect_ratio = (height > 0)
? static_cast<float>(width) / static_cast<float>(height)
: 1.0f;
return glm::perspective(glm::radians(fov), aspect_ratio, near_plane, far_plane);
}
glm::mat4 Camera::get_view_projection_matrix(int width, int height) const
{
return get_projection_matrix(width, height) * get_view_matrix();
}
glm::vec3 Camera::get_front() const
{
glm::vec3 front;
front.x = std::cos(glm::radians(yaw)) * std::cos(glm::radians(pitch));
front.y = std::sin(glm::radians(pitch));
front.z = std::sin(glm::radians(yaw)) * std::cos(glm::radians(pitch));
return glm::normalize(front);
}
glm::vec3 Camera::get_right() const
{
return glm::normalize(glm::cross(get_front(), glm::vec3(0.0f, 1.0f, 0.0f)));
}
glm::vec3 Camera::get_up() const
{
return glm::normalize(glm::cross(get_right(), get_front()));
}
void Camera::rotate(float delta_yaw, float delta_pitch)
{
yaw += delta_yaw;
pitch += delta_pitch;
pitch = std::clamp(pitch, -89.0f, 89.0f);
}
void Camera::look_at(const glm::vec3& target)
{
glm::vec3 direction = glm::normalize(target - position);
pitch = glm::degrees(std::asin(direction.y));
yaw = glm::degrees(std::atan2(direction.z, direction.x));
pitch = std::clamp(pitch, -89.0f, 89.0f);
}
} // namespace Fan

44
src/camera.hpp Normal file
View File

@@ -0,0 +1,44 @@
#ifndef FAN_CAMERA_HPP_
#define FAN_CAMERA_HPP_
#include <glm/glm.hpp>
#include "config.hpp"
namespace Fan
{
class Camera
{
public:
glm::vec3 position{0.0f, 0.0f, 0.0f};
// Euler angles
float yaw{-90.0f};
float pitch{0.0f};
// Projection settings
float fov{CAMERA_FOV};
float near_plane{CAMERA_NEAR};
float far_plane{CAMERA_FAR};
Camera() = default;
explicit Camera(glm::vec3 position) : position(position) {}
// Matrix generation
glm::mat4 get_view_matrix() const;
glm::mat4 get_projection_matrix(int width, int height) const;
glm::mat4 get_view_projection_matrix(int width, int height) const;
// Direction vectors
glm::vec3 get_front() const;
glm::vec3 get_right() const;
glm::vec3 get_up() const;
// Helper functions
void rotate(float delta_yaw, float delta_pitch);
void look_at(const glm::vec3& target);
};
} // namespace Fan
#endif // FAN_CAMERA_HPP_

21
src/config.hpp Normal file
View File

@@ -0,0 +1,21 @@
#ifndef FAN_CONFIG_HPP_
#define FAN_CONFIG_HPP_
namespace Fan
{
inline constexpr const char* VERSION_STRING = "Fan Engine - indev - OpenGL 3.3";
inline constexpr int WINDOW_WIDTH = 1200;
inline constexpr int WINDOW_HEIGHT = 800;
inline constexpr float CAMERA_FOV = 45.0f;
inline constexpr float CAMERA_SPEED = 2.5f;
inline constexpr float CAMERA_SENSITIVITY = 0.1f;
inline constexpr float CAMERA_NEAR = 0.1f;
inline constexpr float CAMERA_FAR = 100.0f;
} // namespace Fan
#endif // FAN_CONFIG_HPP_

26
src/entity.hpp Normal file
View File

@@ -0,0 +1,26 @@
#ifndef FAN_ENTITY_HPP_
#define FAN_ENTITY_HPP_
#include "transform.hpp"
#include "mesh.hpp"
#include "material.hpp"
#include "utils.hpp"
namespace Fan
{
class Entity
{
public:
Transform transform{};
Rc<Mesh> mesh;
Rc<Material> material;
Entity(Rc<Mesh> mesh, Rc<Material> material)
: mesh(mesh)
, material(material) {}
};
} // namespace Fan
#endif // FAN_ENTITY_HPP_

21
src/fan.hpp Normal file
View File

@@ -0,0 +1,21 @@
#ifndef FAN_HPP_
#define FAN_HPP_
#include <camera.hpp>
#include <config.hpp>
#include <entity.hpp>
#include <framebuffer.hpp>
#include <light.hpp>
#include <loader.hpp>
#include <material.hpp>
#include <mesh.hpp>
#include <renderer.hpp>
#include <scene.hpp>
#include <shader.hpp>
#include <texture.hpp>
#include <transform.hpp>
#include <utils.hpp>
#include <vertex.hpp>
#include <window.hpp>
#endif // FAN_HPP_

115
src/framebuffer.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include <iostream>
#include <utility>
#include "framebuffer.hpp"
namespace Fan
{
Rc<FrameBuffer> FrameBuffer::create(GLsizei width, GLsizei height)
{
GLuint id, texture, render_buffer;
glGenFramebuffers(1, &id);
glBindFramebuffer(GL_FRAMEBUFFER, id);
// Create the color texture attachment
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
texture, 0);
// Create a renderbuffer object for depth and stencil attachment
glGenRenderbuffers(1, &render_buffer);
glBindRenderbuffer(GL_RENDERBUFFER, render_buffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, render_buffer);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
glDeleteRenderbuffers(1, &render_buffer);
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &id);
std::cerr << "ERROR: FRAMEBUFFER: Could not create incomplete framebuffer" << std::endl;
return nullptr;
}
return make_rc<FrameBuffer>(id, width, height, texture, render_buffer);
}
FrameBuffer::~FrameBuffer()
{
glDeleteRenderbuffers(1, &m_render_buffer);
glDeleteTextures(1, &m_texture);
glDeleteFramebuffers(1, &m_id);
}
FrameBuffer::FrameBuffer(FrameBuffer&& other) noexcept
: m_id(std::exchange(other.m_id, 0))
, m_width(std::exchange(other.m_width, 0))
, m_height(std::exchange(other.m_height, 0))
, m_texture(std::exchange(other.m_texture, 0))
, m_render_buffer(std::exchange(other.m_render_buffer, 0))
{
}
FrameBuffer& FrameBuffer::operator=(FrameBuffer&& other) noexcept
{
if (this == &other) { return *this; }
std::swap(m_id, other.m_id);
std::swap(m_width, other.m_width);
std::swap(m_height, other.m_height);
std::swap(m_texture, other.m_texture);
std::swap(m_render_buffer, other.m_render_buffer);
return *this;
}
GLuint FrameBuffer::get_texture_id() const
{
return m_texture;
}
GLsizei FrameBuffer::get_width() const
{
return m_width;
}
GLsizei FrameBuffer::get_height() const
{
return m_height;
}
void FrameBuffer::bind() const
{
glBindFramebuffer(GL_FRAMEBUFFER, m_id);
}
void FrameBuffer::unbind() const
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void FrameBuffer::resize(GLsizei width, GLsizei height)
{
m_width = width;
m_height = height;
// Resize the color attachment
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_width, m_height, 0,
GL_RGB, GL_UNSIGNED_BYTE, nullptr);
// Resize the depth/stencil attachment
glBindRenderbuffer(GL_RENDERBUFFER, m_render_buffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8,
m_width, m_height);
}
} // namespace Fan

48
src/framebuffer.hpp Normal file
View File

@@ -0,0 +1,48 @@
#ifndef FRAMEBUFFER_HPP_
#define FRAMEBUFFER_HPP_
#include <glad/gl.h>
#include "utils.hpp"
namespace Fan
{
class FrameBuffer
{
public:
static Rc<FrameBuffer> create(GLsizei width, GLsizei height);
FrameBuffer(GLuint id, GLsizei width, GLsizei height,
GLuint texture, GLuint render_buffer)
: m_id{id}
, m_width{width}
, m_height{height}
, m_texture{texture}
, m_render_buffer{render_buffer} {}
~FrameBuffer();
FrameBuffer(const FrameBuffer&) = delete;
FrameBuffer& operator=(const FrameBuffer&) = delete;
FrameBuffer(FrameBuffer&& other) noexcept;
FrameBuffer& operator=(FrameBuffer&& other) noexcept;
GLuint get_texture_id() const;
GLsizei get_width() const;
GLsizei get_height() const;
void bind() const;
void unbind() const;
void resize(GLsizei width, GLsizei height);
private:
GLuint m_id{};
GLsizei m_width{};
GLsizei m_height{};
GLuint m_texture{};
GLuint m_render_buffer{};
};
} // namespace Fan
#endif // FRAMEBUFFER_HPP_

58
src/light.hpp Normal file
View File

@@ -0,0 +1,58 @@
#ifndef FAN_LIGHT_HPP_
#define FAN_LIGHT_HPP_
#include <vector>
#include <glm/glm.hpp>
namespace Fan
{
struct DirectionalLight
{
glm::vec3 direction{-0.2f, -1.0f, -0.3f};
glm::vec3 ambient{0.05f, 0.05f, 0.05f};
glm::vec3 diffuse{0.4f, 0.4f, 0.4f};
glm::vec3 specular{0.5f, 0.5f, 0.5f};
};
struct PointLight
{
glm::vec3 position{0.0f, 0.0f, 0.0f};
float constant{1.0f};
float linear{0.09f};
float quadratic{0.032f};
glm::vec3 ambient{0.05f, 0.05f, 0.05f};
glm::vec3 diffuse{0.8f, 0.8f, 0.8f};
glm::vec3 specular{1.0f, 1.0f, 1.0f};
};
struct SpotLight
{
glm::vec3 position{0.0f, 0.0f, 0.0f};
glm::vec3 direction{0.0f, 0.0f, -1.0f};
float cut_off{glm::cos(glm::radians(12.5f))};
float outer_cut_off{glm::cos(glm::radians(15.0f))};
float constant{1.0f};
float linear{0.09f};
float quadratic{0.032f};
glm::vec3 ambient{0.0f, 0.0f, 0.0f};
glm::vec3 diffuse{1.0f, 1.0f, 1.0f};
glm::vec3 specular{1.0f, 1.0f, 1.0f};
};
struct LightEnvironment
{
DirectionalLight directional_light;
std::vector<PointLight> point_lights;
std::vector<SpotLight> spot_lights;
};
} // namespace Fan
#endif // FAN_LIGHT_HPP_

120
src/loader.cpp Normal file
View File

@@ -0,0 +1,120 @@
#include <filesystem>
#include <iostream>
#include <unordered_map>
#include <tiny_obj_loader.h>
#include "loader.hpp"
namespace Fan
{
struct VertexIndexHash {
size_t operator()(const tinyobj::index_t& index) const {
return ((std::hash<int>()(index.vertex_index) ^
(std::hash<int>()(index.normal_index) << 1)) >> 1) ^
(std::hash<int>()(index.texcoord_index) << 1);
}
};
struct VertexIndexEqual {
bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const {
return a.vertex_index == b.vertex_index &&
a.normal_index == b.normal_index &&
a.texcoord_index == b.texcoord_index;
}
};
Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader)
{
namespace fs = std::filesystem;
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string err;
fs::path parent_path = fs::path{file_path}.parent_path();
parent_path += fs::path::preferred_separator;
std::string base_dir = parent_path.string();
bool ok = tinyobj::LoadObj(&attrib, &shapes, &materials, &err,
file_path.c_str(), base_dir.c_str());
if (!ok || !err.empty()) {
std::cerr << "ERROR: LOAD_OBJ: Could not load " << file_path << std::endl << err;
return std::nullopt;
}
// Build the unified mesh
std::vector<Vertex> vertices;
std::vector<GLuint> indices;
std::unordered_map<tinyobj::index_t, GLuint,
VertexIndexHash, VertexIndexEqual> unique_vertices;
for (const auto& shape : shapes) {
for (const auto& index : shape.mesh.indices) {
if (unique_vertices.count(index) == 0) {
Vertex vertex{};
vertex.position = {
attrib.vertices[3 * index.vertex_index + 0],
attrib.vertices[3 * index.vertex_index + 1],
attrib.vertices[3 * index.vertex_index + 2]
};
if (index.normal_index >= 0) {
vertex.normal = {
attrib.normals[3 * index.normal_index + 0],
attrib.normals[3 * index.normal_index + 1],
attrib.normals[3 * index.normal_index + 2]
};
}
if (index.texcoord_index >= 0) {
vertex.texture_coords = {
attrib.texcoords[2 * index.texcoord_index + 0],
1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
};
}
unique_vertices[index] = static_cast<GLuint>(vertices.size());
vertices.push_back(vertex);
}
indices.push_back(unique_vertices[index]);
}
}
auto mesh = Mesh::create(vertices, indices);
if (!mesh) {
std::cerr << "ERROR: LOAD_OBJ: Could not create mesh" << std::endl;
return std::nullopt;
}
auto material = make_rc<Material>();
material->shader = shader; // Assigned required shader
// If the .obj file had an .mtl file attached to it
if (!materials.empty()) {
// Take only the first material found
const tinyobj::material_t& mtl = materials[0];
if (!mtl.diffuse_texname.empty()) {
material->diffuse_map = Texture::create(base_dir + mtl.diffuse_texname);
if (!material->diffuse_map) {
std::cerr << "WARNING: LOAD_OBJ: Could not create diffuse texture" << std::endl;
}
}
if (!mtl.specular_texname.empty()) {
material->specular_map = Texture::create(base_dir + mtl.specular_texname);
if (!material->specular_map) {
std::cerr << "WARNING: LOAD_OBJ: Could not create specular texture" << std::endl;
}
}
material->shininess = mtl.shininess;
}
return Entity{mesh, material};
}
} // namespace Fan

16
src/loader.hpp Normal file
View File

@@ -0,0 +1,16 @@
#ifndef FAN_LOADER_HPP_
#define FAN_LOADER_HPP_
#include <string>
#include "entity.hpp"
#include "utils.hpp"
namespace Fan
{
Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader);
} // namespace Fan
#endif // FAN_LOADER_HPP_

364
src/main.cpp Normal file
View File

@@ -0,0 +1,364 @@
#include <cstdio>
#include <vector>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#define IMGUI_DEFINE_MATH_OPERATORS
#include <imgui.h>
#include <imgui_internal.h>
#include <fan.hpp>
using namespace Fan;
struct UserData
{
Camera& camera;
float delta_time{};
bool mouse_grab{};
float speed{CAMERA_SPEED};
float sensitivity{CAMERA_SENSITIVITY};
float last_mouse_x{};
float last_mouse_y{};
bool first_mouse{true};
bool show_debug{};
std::vector<Rc<Scene>> scenes{};
size_t selected_scene_index{};
};
void process_input(Window& window, Camera& camera, UserData& user_data);
void mouse_callback(double pos_x, double pos_y, void* user_data);
void setup_dockspace();
void draw_panel_window(Camera& camera, UserData& user_data);
void draw_viewport_window(FrameBuffer& framebuffer, Window& window, UserData& user_data);
std::vector<Vertex> get_cube_vertices();
template <typename... Args>
void overlay_text(float offset_x, float offset_y, ImVec4 color, const char* format, Args... args)
{
char buffer[256];
std::snprintf(buffer, sizeof(buffer), format, args...);
ImVec2 pos = ImGui::GetWindowPos() + ImVec2{offset_x, offset_y};
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 text_size = ImGui::CalcTextSize(buffer);
draw_list->AddRectFilled(pos - ImVec2{4, 2}, pos + text_size + ImVec2{4, 2}, IM_COL32(0, 0, 0, 160));
draw_list->AddText(pos, ImGui::ColorConvertFloat4ToU32(color), buffer);
}
int main()
{
/******************** Initialization ********************/
auto window = Window::create(WINDOW_WIDTH, WINDOW_HEIGHT, VERSION_STRING);
if (!window) return EXIT_FAILURE;
window->set_vsync(true);
Renderer::init();
Camera camera{{5.0f, 5.0f, 5.0f}};
camera.look_at({0.0f, 0.0f, 0.0f});
UserData user_data{camera};
window->set_callback_user_data(&user_data);
window->set_mouse_callback(mouse_callback);
/******************** Resource loading ********************/
auto shader = Shader::create("./src/shaders/default.vert", "./src/shaders/default.frag");
auto debug_shader = Shader::create("./src/shaders/debug.vert", "./src/shaders/debug.frag");
auto backpack = load_obj("./assets/backpack/backpack.obj", shader);
auto cube_mesh = Mesh::create(get_cube_vertices());
auto cube_texture = Texture::create("./assets/container.jpg");
auto default_mesh = Mesh::create(get_cube_vertices());
auto default_texture = Texture::create("./assets/debugempty.png");
auto framebuffer = FrameBuffer::create(window->get_width(), window->get_height());
if (!shader || !debug_shader
|| !backpack
|| !cube_mesh || !cube_texture
|| !default_mesh || !default_texture
|| !framebuffer) {
return EXIT_FAILURE;
}
/******************** Scene construction ********************/
// Backpack scene
Rc<Scene> backpack_scene = make_rc<Scene>();
user_data.scenes.push_back(backpack_scene);
backpack_scene->add_entity(*backpack);
auto default_material = make_rc<Material>();
default_material->shader = shader;
default_material->diffuse_map = default_texture;
Entity floor{cube_mesh, default_material};
floor.transform.position = {0.0f, -3.0f, 0.0f};
floor.transform.scale = {20.0f, 1.0f, 20.0f};
backpack_scene->add_entity(floor);
PointLight light_white;
light_white.position = glm::vec3(0.0f, 3.0f, 3.0f);
light_white.diffuse = glm::vec3(2.0f, 2.0f, 2.0f); // Bright white
light_white.specular = glm::vec3(2.0f, 2.0f, 2.0f); // White highlights
backpack_scene->light_environment.point_lights.push_back(light_white);
// Cubes scene
Rc<Scene> cubes_scene = make_rc<Scene>();
user_data.scenes.push_back(cubes_scene);
auto cube_material = make_rc<Material>();
cube_material->shader = shader;
cube_material->diffuse_map = cube_texture;
std::vector<glm::vec3> positions{
{ 0.0f, 0.0f, 0.0f}, { 2.0f, 5.0f, -15.0f}, {-1.5f, -2.2f, -2.5f},
{-3.8f, -2.0f, -12.3f}, { 2.4f, -0.4f, -3.5f}, {-1.7f, 3.0f, -7.5f},
{ 1.3f, -2.0f, -2.5f}, { 1.5f, 2.0f, -2.5f}, { 1.5f, 0.2f, -1.5f},
{-1.3f, 1.0f, -1.5f}
};
for (size_t i = 0; i < positions.size(); ++i) {
Entity entity{cube_mesh, cube_material};
entity.transform.position = positions[i];
entity.transform.set_rotation(glm::radians(20.0f * i), {1.0f, 0.3f, 0.5f});
cubes_scene->add_entity(entity);
}
PointLight light_warm;
light_warm.position = glm::vec3(0.0f, 2.0f, 2.0f);
light_warm.diffuse = glm::vec3(1.0f, 0.6f, 0.2f); // Orange
light_warm.specular = glm::vec3(1.0f, 0.8f, 0.5f);
PointLight light_cool;
light_cool.position = glm::vec3(-3.0f, 1.0f, -5.0f);
light_cool.diffuse = glm::vec3(0.2f, 0.5f, 1.0f); // Blue
light_cool.specular = glm::vec3(0.5f, 0.7f, 1.0f);
cubes_scene->light_environment.point_lights.push_back(light_warm);
cubes_scene->light_environment.point_lights.push_back(light_cool);
/******************** Main loop ********************/
while (!window->should_close()) {
window->begin();
window->update();
user_data.delta_time = window->get_delta_time();
process_input(*window, camera, user_data);
Renderer::set_clear_color({0.0f, 0.0f, 0.0f, 1.0f});
Renderer::clear();
// UI Layer
setup_dockspace();
draw_panel_window(camera, user_data);
draw_viewport_window(*framebuffer, *window, user_data);
// Scene Rendering Layer
const Scene& scene = *user_data.scenes[user_data.selected_scene_index];
int width = framebuffer->get_width();
int height = framebuffer->get_height();
framebuffer->bind();
Renderer::set_viewport(0, 0, width, height);
Renderer::clear();
Renderer::draw_scene(scene, camera, width, height);
if (user_data.show_debug) {
Renderer::draw_debug_wireframes(scene, camera, width, height,
debug_shader, default_mesh);
}
framebuffer->unbind();
window->end();
}
return EXIT_SUCCESS;
}
void process_input(Window& window, Camera& camera, UserData& user_data)
{
if (window.is_key_pressed(GLFW_KEY_ESCAPE)) window.set_should_close(true);
static bool wireframe = false;
if (window.is_key_pressed(GLFW_KEY_H)) {
wireframe = !wireframe;
Renderer::set_wireframe(wireframe);
}
static bool fullscreen = false;
if (window.is_key_pressed(GLFW_KEY_F11)) {
fullscreen = !fullscreen;
window.set_fullscreen(fullscreen);
}
if (window.is_key_pressed(GLFW_KEY_INSERT)) {
user_data.mouse_grab = !user_data.mouse_grab;
user_data.first_mouse = true;
window.set_mouse_grab(user_data.mouse_grab);
}
if (!user_data.mouse_grab) return;
float velocity = user_data.speed * user_data.delta_time * (window.is_key_down(GLFW_KEY_LEFT_SHIFT) ? 2.0f : 1.0f);
if (window.is_key_down(GLFW_KEY_W)) camera.position += camera.get_front() * velocity;
if (window.is_key_down(GLFW_KEY_S)) camera.position -= camera.get_front() * velocity;
if (window.is_key_down(GLFW_KEY_A)) camera.position -= camera.get_right() * velocity;
if (window.is_key_down(GLFW_KEY_D)) camera.position += camera.get_right() * velocity;
}
void mouse_callback(double pos_x, double pos_y, void* user_data)
{
auto* data = static_cast<UserData*>(user_data);
if (!data->mouse_grab) return;
if (data->first_mouse) {
data->last_mouse_x = pos_x;
data->last_mouse_y = pos_y;
data->first_mouse = false;
}
float delta_x = (pos_x - data->last_mouse_x) * data->sensitivity;
float delta_y = (data->last_mouse_y - pos_y) * data->sensitivity;
data->last_mouse_x = pos_x;
data->last_mouse_y = pos_y;
data->camera.rotate(delta_x, delta_y);
}
void setup_dockspace()
{
ImGuiID dockspace_id = ImGui::GetID("Dockspace");
ImGuiViewport* viewport = ImGui::GetMainViewport();
static bool first_time = true;
if (first_time) {
first_time = false;
ImGui::DockBuilderRemoveNode(dockspace_id);
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->WorkSize);
ImGuiID main_id = dockspace_id;
ImGuiID panel_id = ImGui::DockBuilderSplitNode(main_id, ImGuiDir_Left, 0.25f, nullptr, &main_id);
ImGui::DockBuilderDockWindow("Panel", panel_id);
ImGui::DockBuilderDockWindow("Viewport", main_id);
ImGui::DockBuilderFinish(dockspace_id);
}
ImGui::DockSpaceOverViewport(dockspace_id, viewport, ImGuiDockNodeFlags_None);
}
void draw_panel_window(Camera& camera, UserData& user_data)
{
static auto logo = Texture::create("assets/logo/logo.png");
ImGui::Begin("Panel");
if (logo) ImGui::Image(logo->get_id(), {50.0f, 50.0f});
ImGui::SameLine();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (50.0f - ImGui::GetTextLineHeight()) * 0.5f);
ImGui::Text("Fan Engine");
ImGui::Separator();
ImGui::Text("Press INSERT to toggle camera controls");
ImGui::SliderFloat("Camera X", &camera.position.x, -10.0f, 10.0f);
ImGui::SliderFloat("Camera Y", &camera.position.y, -10.0f, 10.0f);
ImGui::SliderFloat("Camera Z", &camera.position.z, -10.0f, 10.0f);
ImGui::Separator();
ImGui::Checkbox("Show Debug Wireframes", &user_data.show_debug);
ImGui::Separator();
ImGui::Text("Scene Selector");
for (size_t i = 0; i < user_data.scenes.size(); ++i) {
char label[64];
ImFormatString(label, IM_ARRAYSIZE(label), "Scene %zu", i);
if (ImGui::RadioButton(label, user_data.selected_scene_index == i)) {
user_data.selected_scene_index = i;
}
}
ImGui::End();
}
void draw_viewport_window(FrameBuffer& framebuffer, Window& window, UserData& user_data)
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{0.0f, 0.0f});
ImGui::Begin("Viewport");
ImVec2 size = ImGui::GetContentRegionAvail();
if (size.x > 0 && size.y > 0) {
if (size.x != framebuffer.get_width() || size.y != framebuffer.get_height()) {
framebuffer.resize(size.x, size.y);
}
ImGui::Image(framebuffer.get_texture_id(), size, ImVec2{0.0f, 1.0f}, ImVec2{1.0f, 0.0f});
}
// Overlays
float fps = window.get_delta_time() > 0.0f ? 1.0f / window.get_delta_time() : 0.0f;
overlay_text(20.0f, 35.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", VERSION_STRING);
overlay_text(20.0f, 52.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "FPS: %.1f", fps);
overlay_text(20.0f, 69.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Frame time: %.2f ms", window.get_delta_time());
if (user_data.mouse_grab) { overlay_text(20.0f, 86.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "INPUT LOCKED"); }
ImGui::End();
ImGui::PopStyleVar();
}
std::vector<Vertex> get_cube_vertices()
{
return {
// Back Face
{{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}},
{{-0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 0.0f}},
{{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}},
{{-0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 1.0f, 1.0f}},
{{ 0.5f, 0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 1.0f}},
{{ 0.5f, -0.5f, -0.5f}, { 0.0f, 0.0f, -1.0f}, { 0.0f, 0.0f}},
// Front Face
{{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}},
{{ 0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 0.0f}},
{{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}},
{{ 0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 1.0f, 1.0f}},
{{-0.5f, 0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 1.0f}},
{{-0.5f, -0.5f, 0.5f}, { 0.0f, 0.0f, 1.0f}, { 0.0f, 0.0f}},
// Left Face
{{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}},
{{-0.5f, -0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}},
{{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}},
{{-0.5f, 0.5f, 0.5f}, {-1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}},
{{-0.5f, 0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}},
{{-0.5f, -0.5f, -0.5f}, {-1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}},
// Right Face
{{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}},
{{ 0.5f, -0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 0.0f}},
{{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}},
{{ 0.5f, 0.5f, -0.5f}, { 1.0f, 0.0f, 0.0f}, { 1.0f, 1.0f}},
{{ 0.5f, 0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 1.0f}},
{{ 0.5f, -0.5f, 0.5f}, { 1.0f, 0.0f, 0.0f}, { 0.0f, 0.0f}},
// Bottom Face
{{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}},
{{ 0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 1.0f}},
{{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}},
{{ 0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 1.0f, 0.0f}},
{{-0.5f, -0.5f, 0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 0.0f}},
{{-0.5f, -0.5f, -0.5f}, { 0.0f, -1.0f, 0.0f}, { 0.0f, 1.0f}},
// Top Face
{{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}},
{{ 0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 0.0f}},
{{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}},
{{ 0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 1.0f, 1.0f}},
{{-0.5f, 0.5f, -0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 1.0f}},
{{-0.5f, 0.5f, 0.5f}, { 0.0f, 1.0f, 0.0f}, { 0.0f, 0.0f}}
};
}

30
src/material.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include "material.hpp"
namespace Fan
{
void Material::bind() const
{
shader->use();
// Diffuse (slot 0)
if (diffuse_map) {
diffuse_map->bind(0);
} else {
Texture::unbind(0);
}
shader->set_int("u_material.diffuse", 0);
// Specular (slot 1)
if (specular_map) {
specular_map->bind(1);
} else {
Texture::unbind(1);
}
shader->set_int("u_material.specular", 1);
// Shininess
shader->set_float("u_material.shininess", shininess);
}
} // namespace Fan

22
src/material.hpp Normal file
View File

@@ -0,0 +1,22 @@
#ifndef FAN_MATERIAL_HPP_
#define FAN_MATERIAL_HPP_
#include "shader.hpp"
#include "texture.hpp"
namespace Fan
{
struct Material
{
Rc<Shader> shader;
Rc<Texture> diffuse_map;
Rc<Texture> specular_map;
float shininess{32.0f};
void bind() const;
};
} // namespace Fan
#endif // FAN_MATERIAL_HPP_

86
src/mesh.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include <cstddef>
#include <utility>
#include "mesh.hpp"
namespace Fan
{
Rc<Mesh> Mesh::create(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices/* = {}*/)
{
GLuint VBO{}, VAO{}, EBO{};
bool has_indices = !indices.empty();
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]),
&vertices[0], GL_STATIC_DRAW);
// Upload index data
if (has_indices) {
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(indices[0]),
&indices[0], GL_STATIC_DRAW);
}
// Define vertex data layout
GLsizei stride = sizeof(Vertex);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(Vertex, position));
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(Vertex, normal));
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)offsetof(Vertex, texture_coords));
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
return make_rc<Mesh>(vertices, indices, has_indices, VBO, VAO, EBO);
}
Mesh::~Mesh()
{
glDeleteBuffers(1, &m_EBO);
glDeleteBuffers(1, &m_VBO);
glDeleteVertexArrays(1, &m_VAO);
}
Mesh::Mesh(Mesh&& other) noexcept
: m_vertices(std::move(other.m_vertices))
, m_indices(std::move(other.m_indices))
, m_has_indices(std::exchange(other.m_has_indices, false))
, m_VBO(std::exchange(other.m_VBO, 0))
, m_VAO(std::exchange(other.m_VAO, 0))
, m_EBO(std::exchange(other.m_EBO, 0))
{
}
Mesh& Mesh::operator=(Mesh&& other) noexcept
{
if (this == &other) { return *this; }
std::swap(m_vertices, other.m_vertices);
std::swap(m_indices, other.m_indices);
std::swap(m_has_indices, other.m_has_indices);
std::swap(m_VBO, other.m_VBO);
std::swap(m_VAO, other.m_VAO);
std::swap(m_EBO, other.m_EBO);
return *this;
}
void Mesh::draw() const
{
glBindVertexArray(m_VAO);
if (m_has_indices) {
glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT, 0);
} else {
glDrawArrays(GL_TRIANGLES, 0, m_vertices.size());
}
}
} // namespace Fan

49
src/mesh.hpp Normal file
View File

@@ -0,0 +1,49 @@
#ifndef FAN_MESH_HPP_
#define FAN_MESH_HPP_
#include <vector>
#include <glad/gl.h>
#include "vertex.hpp"
#include "utils.hpp"
namespace Fan
{
class Mesh
{
public:
static Rc<Mesh> create(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices = {});
Mesh(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices,
bool has_indices,
GLuint VBO, GLuint VAO, GLuint EBO)
: m_vertices(vertices)
, m_indices(indices)
, m_has_indices(has_indices)
, m_VBO(VBO)
, m_VAO(VAO)
, m_EBO(EBO) {}
~Mesh();
Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete;
Mesh(Mesh&& other) noexcept;
Mesh& operator=(Mesh&& other) noexcept;
void draw() const;
private:
std::vector<Vertex> m_vertices{};
std::vector<GLuint> m_indices{};
bool m_has_indices{};
GLuint m_VBO{};
GLuint m_VAO{};
GLuint m_EBO{};
};
} // namespace Fan
#endif // FAN_MESH_HPP_

166
src/renderer.cpp Normal file
View File

@@ -0,0 +1,166 @@
#include <algorithm>
#include <string>
#include <glad/gl.h>
#include "renderer.hpp"
namespace Fan
{
void Renderer::init()
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
}
void Renderer::set_viewport(int x, int y, int width, int height)
{
glViewport(x, y, width, height);
}
void Renderer::set_clear_color(const glm::vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
}
void Renderer::set_wireframe(bool enabled)
{
glPolygonMode(GL_FRONT_AND_BACK, enabled ? GL_LINE : GL_FILL);
}
void Renderer::clear()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::draw_scene(const Scene& scene, const Camera& camera,
int screen_width, int screen_height)
{
glm::mat4 view_projection = camera.get_view_projection_matrix(screen_width, screen_height);
GLuint last_shader_id = 0;
// TODO: Order entities by material to minimize uniform uploads
for (const Entity& entity : scene.get_entities()) {
if (!entity.mesh || !entity.material || !entity.material->shader) { continue; }
Rc<Shader> shader = entity.material->shader;
GLuint current_shader_id = shader->get_id();
// Only upload scene uniforms if the shader changed
if (current_shader_id != last_shader_id) {
shader->use();
// Upload camera uniforms
shader->set_mat4("u_view_projection", view_projection);
shader->set_vec3("u_view_pos", camera.position);
// Upload lighting uniforms
const LightEnvironment& env = scene.light_environment;
shader->set_vec3("u_directional_light.direction", env.directional_light.direction);
shader->set_vec3("u_directional_light.ambient", env.directional_light.ambient);
shader->set_vec3("u_directional_light.diffuse", env.directional_light.diffuse);
shader->set_vec3("u_directional_light.specular", env.directional_light.specular);
// Note: current limit is set to MAX_POINT_LIGHT_COUNT (8)
// TODO: Define this limits in a common header file
int point_count = std::min(static_cast<int>(env.point_lights.size()), 8);
shader->set_int("u_point_light_count", point_count);
for (int i = 0; i < point_count; ++i) {
std::string prefix = "u_point_lights[" + std::to_string(i) + "].";
shader->set_vec3(prefix + "position", env.point_lights[i].position);
shader->set_float(prefix + "constant", env.point_lights[i].constant);
shader->set_float(prefix + "linear", env.point_lights[i].linear);
shader->set_float(prefix + "quadratic", env.point_lights[i].quadratic);
shader->set_vec3(prefix + "ambient", env.point_lights[i].ambient);
shader->set_vec3(prefix + "diffuse", env.point_lights[i].diffuse);
shader->set_vec3(prefix + "specular", env.point_lights[i].specular);
}
// Note: current limit is set to MAX_SPOT_LIGHT_COUNT (8)
// TODO: Define this limits in a common header file
int spot_count = std::min(static_cast<int>(env.spot_lights.size()), 8);
shader->set_int("u_spot_light_count", spot_count);
for (int i = 0; i < spot_count; ++i) {
std::string prefix = "u_spot_lights[" + std::to_string(i) + "].";
shader->set_vec3(prefix + "position", env.spot_lights[i].position);
shader->set_vec3(prefix + "direction", env.spot_lights[i].direction);
shader->set_float(prefix + "cut_off", env.spot_lights[i].cut_off);
shader->set_float(prefix + "outer_cut_off", env.spot_lights[i].outer_cut_off);
shader->set_float(prefix + "constant", env.spot_lights[i].constant);
shader->set_float(prefix + "linear", env.spot_lights[i].linear);
shader->set_float(prefix + "quadratic", env.spot_lights[i].quadratic);
shader->set_vec3(prefix + "ambient", env.spot_lights[i].ambient);
shader->set_vec3(prefix + "diffuse", env.spot_lights[i].diffuse);
shader->set_vec3(prefix + "specular", env.spot_lights[i].specular);
}
last_shader_id = current_shader_id;
}
// Bind the specific textures and the shininess float
entity.material->bind();
// Upload the model matrix to transform the specific object
shader->set_mat4("u_model", entity.transform.get_model_matrix());
// Dispatch the draw call
entity.mesh->draw();
}
}
void Renderer::draw_debug_wireframes(const Scene& scene, const Camera& camera,
int screen_width, int screen_height,
const Rc<Shader>& debug_shader,
const Rc<Mesh>& default_mesh)
{
debug_shader->use();
debug_shader->set_mat4("u_view_projection", camera.get_view_projection_matrix(screen_width, screen_height));
// Force wireframe mode and disable depth testing so we can see through objects
set_wireframe(true);
glDisable(GL_DEPTH_TEST);
// Draw all standard entities (green)
debug_shader->set_vec3("u_color", glm::vec3(0.0f, 0.7f, 0.0f));
for (const Entity& entity : scene.get_entities()) {
// If an entity has no mesh, use the default debug mesh
auto mesh = entity.mesh ? entity.mesh : default_mesh;
debug_shader->set_mat4("u_model", entity.transform.get_model_matrix());
mesh->draw();
}
// Draw point lights (yellow)
debug_shader->set_vec3("u_color", glm::vec3(0.7f, 0.7f, 0.0f));
for (const PointLight& light : scene.light_environment.point_lights) {
glm::mat4 model = glm::translate(glm::mat4(1.0f), light.position);
model = glm::scale(model, glm::vec3(0.2f));
debug_shader->set_mat4("u_model", model);
default_mesh->draw();
}
// Draw spot lights (cyan)
debug_shader->set_vec3("u_color", glm::vec3(0.0f, 0.7f, 0.7f));
for (const SpotLight& light : scene.light_environment.spot_lights) {
glm::mat4 model = glm::translate(glm::mat4(1.0f), light.position);
model = glm::scale(model, glm::vec3(0.2f));
debug_shader->set_mat4("u_model", model);
default_mesh->draw();
}
// Restore state
glEnable(GL_DEPTH_TEST);
set_wireframe(false);
}
} // namespace Fan

30
src/renderer.hpp Normal file
View File

@@ -0,0 +1,30 @@
#ifndef FAN_RENDERER_HPP_
#define FAN_RENDERER_HPP_
#include "scene.hpp"
#include "camera.hpp"
namespace Fan
{
class Renderer
{
public:
static void init();
static void set_viewport(int x, int y, int width, int height);
static void set_clear_color(const glm::vec4& color);
static void set_wireframe(bool enabled);
static void clear();
static void draw_scene(const Scene& scene, const Camera& camera,
int screen_width, int screen_height);
static void draw_debug_wireframes(const Scene& scene, const Camera& camera,
int screen_width, int screen_height,
const Rc<Shader>& debug_shader,
const Rc<Mesh>& default_mesh);
};
} // namespace Fan
#endif // FAN_RENDERER_HPP_

16
src/scene.cpp Normal file
View File

@@ -0,0 +1,16 @@
#include "scene.hpp"
namespace Fan
{
void Scene::add_entity(const Entity& entity)
{
m_entities.push_back(entity);
}
const std::vector<Entity>& Scene::get_entities() const
{
return m_entities;
}
} // namespace Fan

27
src/scene.hpp Normal file
View File

@@ -0,0 +1,27 @@
#ifndef FAN_SCENE_HPP_
#define FAN_SCENE_HPP_
#include <vector>
#include "entity.hpp"
#include "light.hpp"
namespace Fan
{
class Scene
{
public:
LightEnvironment light_environment{};
void add_entity(const Entity& entity);
const std::vector<Entity>& get_entities() const;
private:
std::vector<Entity> m_entities{};
};
} // namespace Fan
#endif // FAN_SCENE_HPP_

153
src/shader.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include <iostream>
#include <utility>
#include <glm/gtc/type_ptr.hpp>
#include "shader.hpp"
#include "utils.hpp"
namespace Fan
{
Rc<Shader> Shader::create(const std::string& vertex_shader_path,
const std::string& fragment_shader_path)
{
auto vertex_shader = compile_shader(vertex_shader_path, GL_VERTEX_SHADER);
if (!vertex_shader) {
return nullptr;
}
auto fragment_shader = compile_shader(fragment_shader_path, GL_FRAGMENT_SHADER);
if (!fragment_shader) {
glDeleteShader(*vertex_shader);
return nullptr;
}
GLuint shader_program = glCreateProgram();
glAttachShader(shader_program, *vertex_shader);
glAttachShader(shader_program, *fragment_shader);
glLinkProgram(shader_program);
glDeleteShader(*vertex_shader);
glDeleteShader(*fragment_shader);
GLint success;
glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
if (!success) {
GLint log_length = 0;
glGetProgramiv(shader_program, GL_INFO_LOG_LENGTH, &log_length);
if (log_length > 0) {
std::string log(log_length, ' ');
glGetProgramInfoLog(shader_program, log_length, nullptr, &log[0]);
std::cerr << "ERROR: SHADER: Could not link shader program: " << log << std::endl;
}
glDeleteProgram(shader_program);
return nullptr;
}
return make_rc<Shader>(shader_program);
}
Shader::~Shader()
{
glDeleteProgram(m_id);
}
Shader::Shader(Shader&& other) noexcept
: m_id(std::exchange(other.m_id, 0))
{
}
Shader& Shader::operator=(Shader&& other) noexcept
{
if (this == &other) { return *this; }
std::swap(m_id, other.m_id);
return *this;
}
GLuint Shader::get_id() const
{
return m_id;
}
GLint Shader::get_location(const std::string &name)
{
GLint location = glGetUniformLocation(m_id, name.c_str());
if (location == -1) {
std::cerr << "WARNING: SHADER: uniform " << name << " not found" << std::endl;
}
return location;
}
void Shader::set_int(const std::string& name, GLint value)
{
glUniform1i(get_location(name), value);
}
void Shader::set_float(const std::string& name, float value)
{
glUniform1f(get_location(name), value);
}
void Shader::set_vec3(const std::string& name, float x, float y, float z)
{
glUniform3f(get_location(name), x, y, z);
}
void Shader::set_vec3(const std::string& name, const glm::vec3& value)
{
glUniform3f(get_location(name), value.x, value.y, value.z);
}
void Shader::set_mat4(const std::string& name, const glm::mat4& value)
{
GLint location = get_location(name);
glUniformMatrix4fv(location, 1, GL_FALSE, glm::value_ptr(value));
}
Opt<GLuint> Shader::compile_shader(const std::string& source_path,
GLenum shader_type)
{
auto shader_source = read_entire_file(source_path);
if (!shader_source) {
std::cerr << "ERROR: SHADER: Could not read shader " << source_path << std::endl;
return std::nullopt;
}
GLuint shader = glCreateShader(shader_type);
const char *shader_source_str = shader_source->c_str();
glShaderSource(shader, 1, &shader_source_str, nullptr);
glCompileShader(shader);
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
GLint log_length = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length);
if (log_length > 0) {
std::string log(log_length, ' ');
glGetShaderInfoLog(shader, log_length, nullptr, &log[0]);
std::cerr << "ERROR: SHADER: Could not compile shader: " << log << std::endl;
}
glDeleteShader(shader);
return std::nullopt;
}
return shader;
}
void Shader::use() const
{
if (m_id == 0) {
return;
}
glUseProgram(m_id);
}
} // namespace Fan

47
src/shader.hpp Normal file
View File

@@ -0,0 +1,47 @@
#ifndef FAN_SHADER_HPP_
#define FAN_SHADER_HPP_
#include <string>
#include <glad/gl.h>
#include <glm/glm.hpp>
#include "utils.hpp"
namespace Fan
{
class Shader
{
public:
static Rc<Shader> create(const std::string& vertex_path,
const std::string& fragment_path);
Shader(GLuint id) : m_id{id} {}
~Shader();
Shader(const Shader&) = delete;
Shader& operator=(const Shader&) = delete;
Shader(Shader&& other) noexcept;
Shader& operator=(Shader&& other) noexcept;
GLuint get_id() const;
GLint get_location(const std::string &name);
// Uniform helpers
void set_int(const std::string& name, GLint value);
void set_float(const std::string& name, float value);
void set_vec3(const std::string& name, float x, float y, float z);
void set_vec3(const std::string& name, const glm::vec3& value);
void set_mat4(const std::string& name, const glm::mat4& value);
void use() const;
private:
GLuint m_id{};
static Opt<GLuint> compile_shader(const std::string& source_path,
GLenum shader_type);
};
} // namespace Fan
#endif // FAN_SHADER_HPP_

9
src/shaders/debug.frag Normal file
View File

@@ -0,0 +1,9 @@
#version 330 core
out vec4 frag_color;
uniform vec3 u_color;
void main() {
frag_color = vec4(u_color, 1.0);
}

11
src/shaders/debug.vert Normal file
View File

@@ -0,0 +1,11 @@
#version 330 core
layout (location = 0) in vec3 a_position;
uniform mat4 u_model;
uniform mat4 u_view_projection;
void main()
{
gl_Position = u_view_projection * u_model * vec4(a_position, 1.0);
}

140
src/shaders/default.frag Normal file
View File

@@ -0,0 +1,140 @@
#version 330 core
struct Material
{
sampler2D diffuse;
sampler2D specular;
float shininess;
};
struct DirectionalLight
{
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
struct SpotLight {
vec3 position;
vec3 direction;
float cut_off;
float outer_cut_off;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
in vec3 frag_position;
in vec3 normal;
in vec2 texture_coords;
out vec4 frag_color;
uniform vec3 u_view_pos;
uniform Material u_material;
uniform DirectionalLight u_directional_light;
#define MAX_POINT_LIGHT_COUNT 8
uniform PointLight u_point_lights[MAX_POINT_LIGHT_COUNT];
uniform int u_point_light_count;
#define MAX_SPOT_LIGHT_COUNT 8
uniform SpotLight u_spot_lights[MAX_SPOT_LIGHT_COUNT];
uniform int u_spot_light_count;
vec3 calculate_directional_light(DirectionalLight light, vec3 normal, vec3 view_dir);
vec3 calculate_point_light(PointLight light, vec3 normal, vec3 frag_position, vec3 view_dir);
vec3 calculate_spot_light(SpotLight light, vec3 normal, vec3 frag_position, vec3 view_dir);
void main()
{
vec3 norm = normalize(normal);
vec3 view_dir = normalize(u_view_pos - frag_position);
vec3 result = calculate_directional_light(u_directional_light, norm, view_dir);
for (int i = 0; i < u_point_light_count; ++i) {
result += calculate_point_light(u_point_lights[i], norm, frag_position, view_dir);
}
for (int i = 0; i < u_spot_light_count; ++i) {
result += calculate_spot_light(u_spot_lights[i], norm, frag_position, view_dir);
}
frag_color = vec4(result, 1.0);
}
vec3 calculate_directional_light(DirectionalLight light, vec3 normal, vec3 view_dir)
{
vec3 light_dir = normalize(-light.direction);
float diff = max(dot(normal, light_dir), 0.0);
vec3 reflect_dir = reflect(-light_dir, normal);
float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);
vec3 ambient = light.ambient * vec3(texture(u_material.diffuse, texture_coords));
vec3 diffuse = light.diffuse * diff * vec3(texture(u_material.diffuse, texture_coords));
vec3 specular = light.specular * spec * vec3(texture(u_material.specular, texture_coords));
return ambient + diffuse + specular;
}
vec3 calculate_point_light(PointLight light, vec3 normal, vec3 frag_position, vec3 view_dir)
{
vec3 light_dir = normalize(light.position - frag_position);
float diff = max(dot(normal, light_dir), 0.0);
vec3 reflect_dir = reflect(-light_dir, normal);
float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);
float dist = length(light.position - frag_position);
float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * (dist * dist));
vec3 ambient = light.ambient * vec3(texture(u_material.diffuse, texture_coords));
vec3 diffuse = light.diffuse * diff * vec3(texture(u_material.diffuse, texture_coords));
vec3 specular = light.specular * spec * vec3(texture(u_material.specular, texture_coords));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return ambient + diffuse + specular;
}
vec3 calculate_spot_light(SpotLight light, vec3 normal, vec3 frag_position, vec3 view_dir)
{
vec3 light_dir = normalize(light.position - frag_position);
float diff = max(dot(normal, light_dir), 0.0);
vec3 reflect_dir = reflect(-light_dir, normal);
float spec = pow(max(dot(view_dir, reflect_dir), 0.0), u_material.shininess);
float dist = length(light.position - frag_position);
float attenuation = 1.0 / (light.constant + light.linear * dist + light.quadratic * (dist* dist));
float theta = dot(light_dir, normalize(-light.direction));
float epsilon = light.cut_off - light.outer_cut_off;
float intensity = clamp((theta - light.outer_cut_off) / epsilon, 0.0, 1.0);
vec3 ambient = light.ambient * vec3(texture(u_material.diffuse, texture_coords));
vec3 diffuse = light.diffuse * diff * vec3(texture(u_material.diffuse, texture_coords));
vec3 specular = light.specular * spec * vec3(texture(u_material.specular, texture_coords));
ambient *= attenuation * intensity;
diffuse *= attenuation * intensity;
specular *= attenuation * intensity;
return ambient + diffuse + specular;
}

24
src/shaders/default.vert Normal file
View File

@@ -0,0 +1,24 @@
#version 330 core
layout (location = 0) in vec3 a_position;
layout (location = 1) in vec3 a_normal;
layout (location = 2) in vec2 a_texture_coords;
out vec3 frag_position;
out vec3 normal;
out vec2 texture_coords;
uniform mat4 u_model;
uniform mat4 u_view_projection;
void main()
{
gl_Position = u_view_projection * u_model * vec4(a_position, 1.0);
frag_position = vec3(u_model * vec4(a_position, 1.0));
mat3 normal_matrix = mat3(transpose(inverse(u_model))); // TODO: Calculate on CPU for performance
normal = normal_matrix * a_normal;
texture_coords = a_texture_coords;
}

86
src/texture.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include <iostream>
#include <stb_image.h>
#include "texture.hpp"
namespace Fan
{
Rc<Texture> Texture::create(const std::string& image_path)
{
stbi_set_flip_vertically_on_load(true);
int width, height, n_channels;
stbi_uc *image_data = stbi_load(image_path.c_str(), &width, &height, &n_channels, STBI_rgb_alpha);
if (image_data == NULL) {
std::cerr << "ERROR: TEXTURE: Could not load image " << image_path << std::endl;
return nullptr;
}
GLuint texture;
glGenTextures(1, &texture);
if (glGetError() != GL_NO_ERROR) {
std::cerr << "ERROR: TEXTURE: glGenTextures" << std::endl;
stbi_image_free(image_data);
return nullptr;
}
glBindTexture(GL_TEXTURE_2D, texture);
// Texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// Texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(image_data);
return make_rc<Texture>(texture);
}
Texture::~Texture()
{
glDeleteTextures(1, &m_id);
}
Texture::Texture(Texture&& other) noexcept
{
m_id = other.m_id;
other.m_id = 0;
}
Texture& Texture::operator=(Texture&& other) noexcept
{
if (this == &other) {
return *this;
}
glDeleteTextures(1, &m_id);
m_id = other.m_id;
other.m_id = 0;
return *this;
}
GLuint Texture::get_id() const
{
return m_id;
}
void Texture::bind(GLuint slot/* = 0*/) const
{
glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, m_id);
}
void Texture::unbind(GLuint slot/* = 0*/)
{
glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, 0);
}
} // namespace Fan

35
src/texture.hpp Normal file
View File

@@ -0,0 +1,35 @@
#ifndef FAN_TEXTURE_HPP_
#define FAN_TEXTURE_HPP_
#include <string>
#include <glad/gl.h>
#include "utils.hpp"
namespace Fan
{
class Texture
{
public:
static Rc<Texture> create(const std::string& image_path);
Texture(GLuint id) : m_id{id} {}
~Texture();
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
Texture(Texture&& other) noexcept;
Texture& operator=(Texture&& other) noexcept;
GLuint get_id() const;
void bind(GLuint slot = 0) const;
static void unbind(GLuint slot = 0);
private:
GLuint m_id{};
};
} // namespace Fan
#endif // FAN_TEXTURE_HPP_

49
src/transform.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "transform.hpp"
namespace Fan
{
void Transform::rotate(float angle_rad, const glm::vec3& axis)
{
rotation = glm::angleAxis(angle_rad, glm::normalize(axis)) * rotation;
}
glm::mat4 Transform::get_model_matrix() const
{
glm::mat4 model = glm::translate(glm::mat4(1.0f), position);
model *= glm::mat4_cast(rotation);
model = glm::scale(model, scale);
return model;
}
glm::vec3 Transform::get_euler_angles() const
{
return glm::eulerAngles(rotation);
}
glm::vec3 Transform::get_forward() const
{
return rotation * glm::vec3(0.0f, 0.0f, -1.0f);
}
glm::vec3 Transform::get_right() const
{
return rotation * glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 Transform::get_up() const
{
return rotation * glm::vec3(0.0f, 1.0f, 0.0f);
}
void Transform::set_rotation(float angle_rad, const glm::vec3& axis)
{
rotation = glm::angleAxis(angle_rad, glm::normalize(axis));
}
void Transform::set_euler_angles(const glm::vec3& euler_rad)
{
rotation = glm::quat(euler_rad);
}
} // namespace Fan

45
src/transform.hpp Normal file
View File

@@ -0,0 +1,45 @@
#ifndef FAN_TRANSFORM_HPP_
#define FAN_TRANSFORM_HPP_
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
namespace Fan
{
struct Transform
{
glm::vec3 position{0.0f, 0.0f, 0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
Transform() = default;
explicit Transform(const glm::vec3& position)
: position(position) {}
// Rotate relative to current orientation
void rotate(float angle_rad, const glm::vec3& axis);
// Get the model matrix of the transform
glm::mat4 get_model_matrix() const;
// Get Euler angles in radians
glm::vec3 get_euler_angles() const;
// Get direction vectors
glm::vec3 get_forward() const;
glm::vec3 get_right() const;
glm::vec3 get_up() const;
// Set rotation using axis-angle (angle in radians)
void set_rotation(float angle_rad, const glm::vec3& axis);
// Set rotation using Euler angles in radians (pitch, yaw, roll / X, Y, Z)
void set_euler_angles(const glm::vec3& euler_rad);
};
} // namespace Fan
#endif // FAN_TRANSFORM_HPP_

20
src/utils.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include <fstream>
#include <sstream>
#include "utils.hpp"
namespace Fan
{
Opt<std::string> read_entire_file(const std::string& file_path)
{
std::ifstream file(file_path);
if (!file) { return std::nullopt; }
std::ostringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
} // namespace Fan

27
src/utils.hpp Normal file
View File

@@ -0,0 +1,27 @@
#ifndef FAN_UTILS_HPP_
#define FAN_UTILS_HPP_
#include <memory>
#include <optional>
#include <string>
namespace Fan
{
template <typename T>
using Rc = std::shared_ptr<T>;
template <typename T, typename... Args>
Rc<T> make_rc(Args&&... args)
{
return std::make_shared<T>(std::forward<Args>(args)...);
}
template <typename T>
using Opt = std::optional<T>;
Opt<std::string> read_entire_file(const std::string& file_path);
} // namespace Fan
#endif // FAN_UTILS_HPP_

18
src/vertex.hpp Normal file
View File

@@ -0,0 +1,18 @@
#ifndef FAN_VERTEX_HPP_
#define FAN_VERTEX_HPP_
#include <glm/glm.hpp>
namespace Fan
{
struct Vertex
{
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texture_coords;
};
} // namespace Fan
#endif // FAN_VERTEX_HPP_

270
src/window.cpp Normal file
View File

@@ -0,0 +1,270 @@
#include <iostream>
#include <utility>
#include <glad/gl.h>
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#include "window.hpp"
namespace Fan
{
Rc<Window> Window::create(int width, int height, const std::string& title)
{
glfwSetErrorCallback(error_callback);
if (!glfwInit()) {
std::cerr << "ERROR: WINDOW: Could not initialize GLFW" << std::endl;
return nullptr;
}
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow *window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (window == nullptr) {
std::cerr << "ERROR: WINDOW: Could not create window" << std::endl;
glfwTerminate();
return nullptr;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
glfwSetCursorPosCallback(window, mouse_callback);
glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
if (!gladLoadGL(glfwGetProcAddress)) {
std::cerr << "ERROR: WINDOW: Could not initialize OpenGL context" << std::endl;
glfwDestroyWindow(window);
glfwTerminate();
return nullptr;
}
int fb_width, fb_height;
glfwGetFramebufferSize(window, &fb_width, &fb_height);
glViewport(0, 0, fb_width, fb_height);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = nullptr;
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init();
return make_rc<Window>(window, width, height);
}
Window::Window(GLFWwindow *window, int width, int height)
: m_width(width)
, m_height(height)
, m_window(window)
{
// Bind this instance to the window for static callbacks
glfwSetWindowUserPointer(window, this);
};
Window::~Window()
{
if (!is_valid()) { return; }
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(m_window);
glfwTerminate();
}
Window::Window(Window&& other) noexcept
: m_delta_time(other.m_delta_time)
, m_width(other.m_width)
, m_height(other.m_height)
, m_window(std::exchange(other.m_window, nullptr))
, m_last_frame(other.m_last_frame)
, m_callback_user_data(other.m_callback_user_data)
, m_mouse_user_callback(other.m_mouse_user_callback)
, m_current_keys(std::move(other.m_current_keys))
, m_previous_keys(std::move(other.m_previous_keys))
{
if (m_window != nullptr) {
glfwSetWindowUserPointer(m_window, this);
}
}
Window& Window::operator=(Window&& other) noexcept
{
if (this == &other) { return *this; }
std::swap(m_delta_time, other.m_delta_time);
std::swap(m_width, other.m_width);
std::swap(m_height, other.m_height);
std::swap(m_window, other.m_window);
std::swap(m_last_frame, other.m_last_frame);
std::swap(m_callback_user_data, other.m_callback_user_data);
std::swap(m_mouse_user_callback, other.m_mouse_user_callback);
std::swap(m_current_keys, other.m_current_keys);
std::swap(m_previous_keys, other.m_previous_keys);
if (m_window != nullptr) {
glfwSetWindowUserPointer(m_window, this);
}
if (other.m_window != nullptr) {
glfwSetWindowUserPointer(other.m_window, &other);
}
return *this;
}
bool Window::should_close() const
{
return is_valid() ? glfwWindowShouldClose(m_window) : true;
}
bool Window::is_valid() const
{
return m_window != nullptr;
}
bool Window::is_fullscreen() const
{
return is_valid() && (glfwGetWindowMonitor(m_window) != nullptr);
}
bool Window::is_key_down(int glfw_key)
{
return is_valid() && m_current_keys[glfw_key];
}
bool Window::is_key_pressed(int glfw_key)
{
return is_valid() && m_current_keys[glfw_key] && !m_previous_keys[glfw_key];
}
float Window::get_delta_time()
{
return m_delta_time;
}
int Window::get_width() const
{
return m_width;
}
int Window::get_height() const
{
return m_height;
}
void Window::set_should_close(bool enabled)
{
if (is_valid()) { glfwSetWindowShouldClose(m_window, enabled); }
}
void Window::set_mouse_grab(bool enabled)
{
if (is_valid()) {
glfwSetInputMode(m_window, GLFW_CURSOR, enabled ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
}
}
void Window::set_fullscreen(bool enabled)
{
if (!is_valid() || enabled == is_fullscreen()) { return; }
static int windowed_x = 100;
static int windowed_y = 100;
static int windowed_width = 800;
static int windowed_height = 600;
if (enabled) {
glfwGetWindowPos(m_window, &windowed_x, &windowed_y);
glfwGetWindowSize(m_window, &windowed_width, &windowed_height);
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
if (monitor == nullptr) { return; }
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
if (mode == nullptr) { return; }
glfwSetWindowMonitor(m_window, monitor, 0, 0,
mode->width, mode->height, mode->refreshRate);
} else {
glfwSetWindowMonitor(m_window, nullptr, windowed_x, windowed_y,
windowed_width, windowed_height, GLFW_DONT_CARE);
}
}
void Window::set_vsync(bool enabled)
{
if (is_valid()) glfwSwapInterval(enabled ? 1 : 0);
}
void Window::set_callback_user_data(void *data)
{
m_callback_user_data = data;
}
void Window::set_mouse_callback(MouseCallback callback)
{
m_mouse_user_callback = callback;
}
void Window::begin()
{
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
}
void Window::update()
{
if (!is_valid()) { return; }
float current_time = static_cast<float>(glfwGetTime());
m_delta_time = current_time - m_last_frame;
m_last_frame = current_time;
m_previous_keys = m_current_keys;
for (int i = 32; i < 348; ++i) {
m_current_keys[i] = glfwGetKey(m_window, i) == GLFW_PRESS;
}
}
void Window::end()
{
if (!is_valid()) { return; }
glfwGetWindowSize(m_window, &m_width, &m_height);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(m_window);
glfwPollEvents();
}
void Window::error_callback(int error, const char* description)
{
std::cerr << "ERROR: WINDOW: GLFW: code: " << error << " msg: " << description << std::endl;
}
void Window::framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
(void) window;
glViewport(0, 0, width, height);
}
void Window::mouse_callback(GLFWwindow* window, double x_pos, double y_pos)
{
Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (win == nullptr || !win->is_valid() || win->m_mouse_user_callback == nullptr) { return; }
win->m_mouse_user_callback(x_pos, y_pos, win->m_callback_user_data);
}
} // namespace Fan

70
src/window.hpp Normal file
View File

@@ -0,0 +1,70 @@
#ifndef FAN_WINDOW_HPP_
#define FAN_WINDOW_HPP_
#include <string>
#include <unordered_map>
#include <GLFW/glfw3.h>
#include "utils.hpp"
namespace Fan
{
class Window
{
public:
static Rc<Window> create(int width, int height, const std::string& title);
Window(GLFWwindow* window, int width, int height);
~Window();
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
Window(Window&& other) noexcept;
Window& operator=(Window&& other) noexcept;
bool should_close() const;
bool is_valid() const;
bool is_fullscreen() const;
bool is_key_down(int glfw_key);
bool is_key_pressed(int glfw_key);
float get_delta_time();
int get_width() const;
int get_height() const;
void set_should_close(bool enabled);
void set_mouse_grab(bool enabled);
void set_fullscreen(bool enabled);
void set_vsync(bool enabled);
// User callbacks
using MouseCallback = void (*)(double x_pos, double y_pos, void* user_data);
void set_callback_user_data(void *data);
void set_mouse_callback(MouseCallback callback);
// Main loop functions
void begin();
void update();
void end();
private:
float m_delta_time{};
int m_width{};
int m_height{};
GLFWwindow* m_window{};
float m_last_frame{};
void *m_callback_user_data{};
MouseCallback m_mouse_user_callback{};
std::unordered_map<int, bool> m_current_keys{};
std::unordered_map<int, bool> m_previous_keys{};
static void error_callback(int error, const char* description);
static void framebuffer_size_callback(GLFWwindow* window, int width, int height);
static void mouse_callback(GLFWwindow* window, double x_pos, double y_pos);
};
} // namespace Fan
#endif // FAN_WINDOW_HPP_