Add GL error handling, resource fallbacks and GPU timing; speed up lighting

Bug fixes:
- Release static GL objects (UBOs, logo texture) before the context is
  destroyed via new Renderer::shutdown()
- Fix max point/spot light warnings never firing
- Show frame time in ms instead of seconds
- Size the viewport framebuffer in physical pixels on HiDPI displays
- Stop double-flipping OBJ UVs; add flip_uvs option to load_obj for
  models with a top-left UV origin (backpack)

Build:
- Track header dependencies (-MMD -MP) so header edits trigger rebuilds
- Create build/ automatically

Error handling:
- Add gldebug module: KHR_debug output routed to the logger in debug
  builds, object labels, and error/status string helpers
- Validate inputs and check GL errors when creating textures, meshes,
  framebuffers, uniform buffers and shaders; FrameBuffer::resize() and
  Renderer::init() now report failure

Resources:
- Add ResourceCache: shares textures and shaders by path and substitutes
  fallbacks (magenta checkerboard texture, unlit shader, cube mesh) for
  assets that fail to load; specular maps are optional
- load_obj never fails, a broken model becomes a fallback cube
- Renderer draws submeshes with broken materials using the fallback
  material instead of skipping them
- Add Texture::create_from_pixels, Shader::create_from_source and
  Mesh::create_cube
- Cache uniform locations in Shader, looked up with string_view so
  hits don't allocate; missing uniforms warn once

Performance:
- Add CpuTimer and GpuTimer (GL_TIME_ELAPSED, non-blocking readback)
  and a Performance section in the UI with averaged overlay numbers
- Sample material textures once per fragment and apply material colors
  after accumulating lights
- Pass camera position through CameraBlock instead of inverting the
  view matrix per fragment
- Compute the normal matrix on the CPU instead of per vertex
- Skip spot light work for fragments outside the outer cone
This commit is contained in:
2026-10-28 21:23:41 +01:00
parent 642d29634a
commit d081108ea5
39 changed files with 1275 additions and 274 deletions

View File

@@ -2,6 +2,7 @@ CXX := clang++
CXXFLAGS := -Wall -Wextra -std=c++17
CC := clang
CFLAGS := -Wall -Wextra
DEPFLAGS := -MMD -MP
INCLUDES := -I./src/ \
-I./vendor/glad/include/ \
@@ -17,14 +18,17 @@ TARGET := build/fan
OBJS := build/camera.o \
build/framebuffer.o \
build/gldebug.o \
build/loader.o \
build/main.o \
build/material.o \
build/mesh.o \
build/renderer.o \
build/resourcecache.o \
build/scene.o \
build/shader.o \
build/texture.o \
build/timer.o \
build/transform.o \
build/uniformbuffer.o \
build/utils.o \
@@ -33,30 +37,31 @@ OBJS := build/camera.o \
build/stb_image.o \
build/tiny_obj_loader.o \
IMGUI_OBJS := build/imgui.o build/imgui_demo.o build/imgui_draw.o build/imgui_tables.o build/imgui_widgets.o \
IMGUI_OBJS := build/imgui.o build/imgui_demo.o build/imgui_draw.o \
build/imgui_tables.o build/imgui_widgets.o \
build/imgui_impl_glfw.o build/imgui_impl_opengl3.o
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS) $(IMGUI_OBJS)
$(TARGET): $(OBJS) $(IMGUI_OBJS) | build
$(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS)
build/%.o: src/%.cpp
$(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@
build/%.o: src/%.cpp | build
$(CXX) $(CXXFLAGS) $(DEPFLAGS) $(INCLUDES) -c $< -o $@
build/glad.o: vendor/glad/src/gl.c
build/glad.o: vendor/glad/src/gl.c | build
$(CC) $(CFLAGS) -I./vendor/glad/include/ -c $< -o $@
build/stb_image.o: vendor/stb/stb_image.h
build/stb_image.o: vendor/stb/stb_image.h | build
$(CC) $(CFLAGS) -DSTB_IMAGE_IMPLEMENTATION -xc -c $< -o $@
build/tiny_obj_loader.o: vendor/tinyobjloader/tiny_obj_loader.h
build/tiny_obj_loader.o: vendor/tinyobjloader/tiny_obj_loader.h | build
$(CXX) $(CXXFLAGS) -DTINYOBJLOADER_IMPLEMENTATION -xc++ -c $< -o $@
define imgui_rule
build/$(1): vendor/imgui/$(2)
build/$(1): vendor/imgui/$(2) | build
$(CXX) $(CXXFLAGS) -I./vendor/imgui/ -c $$< -o $$@
endef
@@ -68,5 +73,10 @@ $(eval $(call imgui_rule,imgui_widgets.o,imgui_widgets.cpp))
$(eval $(call imgui_rule,imgui_impl_glfw.o,backends/imgui_impl_glfw.cpp))
$(eval $(call imgui_rule,imgui_impl_opengl3.o,backends/imgui_impl_opengl3.cpp))
build:
mkdir -p build
clean:
rm -f build/*.o $(TARGET)
rm -f build/*.o build/*.d $(TARGET)
-include $(OBJS:.o=.d)

View File

@@ -18,7 +18,6 @@ system-wide**, the rest of the dependencies are already included.
After that, building comes down to just running the included Makefile.
```console
$ mkdir build
$ make # optionally pass -j$(nproc)
```
@@ -51,6 +50,7 @@ flowchart TB
Renderer --> Camera
Renderer --> Scene
Renderer -->|Updates| UniformBuffer
Renderer -->|Owns| ResourceCache
end
subgraph World
@@ -89,6 +89,8 @@ flowchart TB
Material --> Shader
Material --> Texture
UniformBuffer -->|Global data| Shader
ResourceCache -.->|Caches, falls back| Texture
ResourceCache -.->|Caches, falls back| Shader
end
end
@@ -101,7 +103,7 @@ flowchart TB
classDef loading fill:#d97706,stroke:#92400e,color:#fff
classDef platform fill:#64748b,stroke:#334155,color:#fff
class Renderer,Scene,Camera,Framebuffer application
class Renderer,Scene,Camera,Framebuffer,ResourceCache application
class Entity,Transform,Mesh,SubMesh,Material,LightEnvironment world
class UniformBuffer,Shader,Texture,Vertices,Indices,DirectionalLight,PointLight,SpotLight data
class Loader,tinyobjloader,stb_image loading

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 B

View File

@@ -12,19 +12,20 @@ class Camera
public:
glm::vec3 position{0.0f, 0.0f, 0.0f};
// Euler angles
// Euler angles in degrees (unlike Transform, which uses radians).
// yaw = -90 looks down -Z
float yaw{-90.0f};
float pitch{0.0f};
// Projection settings
float fov{CAMERA_FOV};
float fov{CAMERA_FOV}; // Vertical, in degrees
float near_plane{CAMERA_NEAR};
float far_plane{CAMERA_FAR};
Camera() = default;
explicit Camera(glm::vec3 position) : position(position) {}
// Matrix generation
// Matrix generation (width and height only set the aspect ratio)
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;
@@ -34,9 +35,9 @@ public:
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);
// Helper functions. Both clamp pitch to [-89, 89] to avoid flipping at the poles
void rotate(float delta_yaw, float delta_pitch); // Deltas in degrees
void look_at(const glm::vec3& target); // Sets yaw and pitch, keeps position
};
} // namespace Fan

View File

@@ -9,13 +9,14 @@ 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_FOV = 45.0f; // Vertical, in degrees
inline constexpr float CAMERA_SPEED = 2.5f;
inline constexpr float CAMERA_SENSITIVITY = 0.1f;
inline constexpr float CAMERA_SPEED = 2.5f; // Units per second
inline constexpr float CAMERA_SENSITIVITY = 0.1f; // Degrees per pixel of mouse movement
inline constexpr float CAMERA_NEAR = 0.1f;
inline constexpr float CAMERA_FAR = 100.0f;
// Must match the #defines in shaders/default.frag
inline constexpr int MAX_POINT_LIGHTS = 32;
inline constexpr int MAX_SPOT_LIGHTS = 32;

View File

@@ -14,7 +14,7 @@ class Entity
public:
Transform transform{};
Rc<Mesh> mesh;
Rc<Material> material{}; // Optional material override
Rc<Material> material{}; // Optional, replaces the material of every SubMesh
Entity(Rc<Mesh> mesh, Rc<Material> material = nullptr)
: mesh(mesh)

View File

@@ -5,14 +5,17 @@
#include <config.hpp>
#include <entity.hpp>
#include <framebuffer.hpp>
#include <gldebug.hpp>
#include <light.hpp>
#include <loader.hpp>
#include <material.hpp>
#include <mesh.hpp>
#include <renderer.hpp>
#include <resourcecache.hpp>
#include <scene.hpp>
#include <shader.hpp>
#include <texture.hpp>
#include <timer.hpp>
#include <transform.hpp>
#include <uniformbuffer.hpp>
#include <utils.hpp>

View File

@@ -1,6 +1,7 @@
#include <utility>
#include "framebuffer.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -8,6 +9,13 @@ namespace Fan
Rc<FrameBuffer> FrameBuffer::create(GLsizei width, GLsizei height)
{
if (!is_valid_size(width, height)) {
Log::error() << "Could not create framebuffer: invalid size " << width << "x" << height;
return nullptr;
}
gl_clear_errors();
GLuint id, texture, render_buffer;
glGenFramebuffers(1, &id);
@@ -31,14 +39,24 @@ Rc<FrameBuffer> FrameBuffer::create(GLsizei width, GLsizei height)
GL_RENDERBUFFER, render_buffer);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
GLenum err = gl_pop_error();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) {
glDeleteRenderbuffers(1, &render_buffer);
glDeleteTextures(1, &texture);
glDeleteFramebuffers(1, &id);
Log::error() << "Could not create incomplete framebuffer";
Log::error() << "Could not create framebuffer: " << gl_framebuffer_status_string(status)
<< ", " << gl_error_string(err);
return nullptr;
}
set_gl_label(GL_FRAMEBUFFER, id, "FrameBuffer");
set_gl_label(GL_TEXTURE, texture, "FrameBuffer color");
set_gl_label(GL_RENDERBUFFER, render_buffer, "FrameBuffer depth/stencil");
Log::info() << "Created framebuffer (" << width << "x" << height << ")";
return make_rc<FrameBuffer>(id, width, height, texture, render_buffer);
}
@@ -97,8 +115,15 @@ void FrameBuffer::unbind() const
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
void FrameBuffer::resize(GLsizei width, GLsizei height)
bool FrameBuffer::resize(GLsizei width, GLsizei height)
{
if (!is_valid_size(width, height)) {
Log::error() << "Could not resize framebuffer: invalid size " << width << "x" << height;
return false;
}
gl_clear_errors();
m_width = width;
m_height = height;
@@ -113,6 +138,31 @@ void FrameBuffer::resize(GLsizei width, GLsizei height)
m_width, m_height);
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// Check completeness without disturbing the caller's framebuffer binding
GLint previous_fbo = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previous_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_id);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previous_fbo));
GLenum err = gl_pop_error();
if (status != GL_FRAMEBUFFER_COMPLETE || err != GL_NO_ERROR) {
Log::error() << "Could not resize framebuffer to " << width << "x" << height
<< ": " << gl_framebuffer_status_string(status)
<< ", " << gl_error_string(err);
return false;
}
return true;
}
bool FrameBuffer::is_valid_size(GLsizei width, GLsizei height)
{
GLint max_size = 0;
glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &max_size);
return width > 0 && height > 0 && width <= max_size && height <= max_size;
}
} // namespace Fan

View File

@@ -8,6 +8,8 @@
namespace Fan
{
// Off-screen render target: an RGB8 color texture (see get_texture_id) and a
// depth/stencil renderbuffer. Sizes are in pixels
class FrameBuffer
{
public:
@@ -31,8 +33,11 @@ public:
GLsizei get_height() const;
void bind() const;
void unbind() const;
void resize(GLsizei width, GLsizei height);
void unbind() const; // Binds the default framebuffer (the window)
// Reallocates the attachments, discarding their contents. Returns false if
// the new size is invalid or the framebuffer became incomplete
bool resize(GLsizei width, GLsizei height);
private:
GLuint m_id{};
@@ -41,6 +46,8 @@ private:
GLuint m_texture{};
GLuint m_render_buffer{};
static bool is_valid_size(GLsizei width, GLsizei height);
};
} // namespace Fan

128
src/gldebug.cpp Normal file
View File

@@ -0,0 +1,128 @@
#include <algorithm>
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
{
static const char* debug_source_string(GLenum source)
{
switch (source) {
case GL_DEBUG_SOURCE_API: return "API";
case GL_DEBUG_SOURCE_WINDOW_SYSTEM: return "Window system";
case GL_DEBUG_SOURCE_SHADER_COMPILER: return "Shader compiler";
case GL_DEBUG_SOURCE_THIRD_PARTY: return "Third party";
case GL_DEBUG_SOURCE_APPLICATION: return "Application";
default: return "Other";
}
}
static const char* debug_type_string(GLenum type)
{
switch (type) {
case GL_DEBUG_TYPE_ERROR: return "error";
case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: return "deprecated";
case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: return "undefined behavior";
case GL_DEBUG_TYPE_PORTABILITY: return "portability";
case GL_DEBUG_TYPE_PERFORMANCE: return "performance";
default: return "other";
}
}
static void GLAD_API_PTR debug_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length,
const GLchar* message, const void* user_param)
{
(void) length;
(void) user_param;
LogStream (*log)() = Log::trace;
switch (severity) {
case GL_DEBUG_SEVERITY_HIGH: log = Log::error; break;
case GL_DEBUG_SEVERITY_MEDIUM: log = Log::warn; break;
case GL_DEBUG_SEVERITY_LOW: log = Log::debug; break;
default: break;
}
// Output is synchronous: put a breakpoint here to see the offending call in the stack
log() << "OpenGL " << debug_source_string(source) << " " << debug_type_string(type)
<< " (" << id << "): " << message;
}
void enable_gl_debug_output()
{
if (!GLAD_GL_KHR_debug) {
Log::warn() << "KHR_debug not supported, OpenGL debug output disabled";
return;
}
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(debug_callback, nullptr);
// Notifications are mostly driver chatter (buffer placement, shader recompiles...)
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DEBUG_SEVERITY_NOTIFICATION,
0, nullptr, GL_FALSE);
Log::info() << "Enabled OpenGL debug output";
}
void set_gl_label(GLenum identifier, GLuint id, const std::string& label)
{
if (!GLAD_GL_KHR_debug) { return; }
// Labels must be shorter than GL_MAX_LABEL_LENGTH
GLint max_length = 0;
glGetIntegerv(GL_MAX_LABEL_LENGTH, &max_length);
GLsizei length = std::min(static_cast<GLsizei>(label.size()), max_length - 1);
glObjectLabel(identifier, id, length, label.c_str());
}
void gl_clear_errors()
{
gl_pop_error();
}
GLenum gl_pop_error()
{
GLenum first = glGetError();
if (first == GL_NO_ERROR) { return first; }
// Several error flags can be set at once, drain them so they aren't
// blamed on a later call (bounded in case the driver keeps reporting)
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {}
return first;
}
const char* gl_error_string(GLenum error)
{
switch (error) {
case GL_NO_ERROR: return "GL_NO_ERROR";
case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
case GL_INVALID_VALUE: return "GL_INVALID_VALUE";
case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION";
case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
default: return "Unknown OpenGL error";
}
}
const char* gl_framebuffer_status_string(GLenum status)
{
switch (status) {
case GL_FRAMEBUFFER_COMPLETE: return "GL_FRAMEBUFFER_COMPLETE";
case GL_FRAMEBUFFER_UNDEFINED: return "GL_FRAMEBUFFER_UNDEFINED";
case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT";
case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT";
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER";
case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: return "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER";
case GL_FRAMEBUFFER_UNSUPPORTED: return "GL_FRAMEBUFFER_UNSUPPORTED";
case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: return "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE";
default: return "Unknown framebuffer status";
}
}
} // namespace Fan

27
src/gldebug.hpp Normal file
View File

@@ -0,0 +1,27 @@
#ifndef FAN_GLDEBUG_HPP_
#define FAN_GLDEBUG_HPP_
#include <string>
#include <glad/gl.h>
namespace Fan
{
// Route driver messages to the logger through KHR_debug (no-op if unsupported)
void enable_gl_debug_output();
// Name a GL object so debug messages and tools like RenderDoc show it
void set_gl_label(GLenum identifier, GLuint id, const std::string& label);
// Discard errors left over from earlier calls
void gl_clear_errors();
// Drain the error queue and return the first error (GL_NO_ERROR if none)
GLenum gl_pop_error();
const char* gl_error_string(GLenum error);
const char* gl_framebuffer_status_string(GLenum status);
} // namespace Fan
#endif // FAN_GLDEBUG_HPP_

View File

@@ -7,9 +7,12 @@
namespace Fan
{
// Attenuation of point and spot lights at distance d:
// 1 / (constant + linear * d + quadratic * d^2)
struct DirectionalLight
{
glm::vec3 direction{-0.2f, -1.0f, -0.3f};
glm::vec3 direction{-0.2f, -1.0f, -0.3f}; // Direction the light travels, normalized in the shader
glm::vec3 ambient{0.05f, 0.05f, 0.05f};
glm::vec3 diffuse{0.4f, 0.4f, 0.4f};
@@ -34,6 +37,8 @@ struct SpotLight
glm::vec3 position{0.0f, 0.0f, 0.0f};
glm::vec3 direction{0.0f, 0.0f, -1.0f};
// Cosines (not angles) of the inner and outer cone half-angles. The light
// fades out between the two
float cut_off{glm::cos(glm::radians(12.5f))};
float outer_cut_off{glm::cos(glm::radians(15.0f))};
@@ -49,6 +54,8 @@ struct SpotLight
struct LightEnvironment
{
DirectionalLight directional_light;
// Lights past MAX_POINT_LIGHTS / MAX_SPOT_LIGHTS are ignored by the renderer
std::vector<PointLight> point_lights;
std::vector<SpotLight> spot_lights;
};

View File

@@ -3,13 +3,15 @@
#include <tiny_obj_loader.h>
#include "loader.hpp"
#include "resourcecache.hpp"
#include "logger.hpp"
namespace Fan
{
struct VertexIndexHash {
size_t operator()(const tinyobj::index_t& index) const {
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);
@@ -17,14 +19,22 @@ struct VertexIndexHash {
};
struct VertexIndexEqual {
bool operator()(const tinyobj::index_t& a, const tinyobj::index_t& b) const {
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)
// Magenta checkerboard cube standing in for a model that failed to load
static Entity create_fallback_entity()
{
return Entity{ResourceCache::get_fallback_mesh(), ResourceCache::get_fallback_material()};
}
Entity load_obj(const std::string& file_path, Rc<Shader> shader,
bool flip_uvs/* = false*/)
{
namespace fs = std::filesystem;
@@ -42,7 +52,7 @@ Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader)
file_path.c_str(), base_dir.c_str());
if (!ok) {
Log::error() << "Could not load " << file_path << ": " << err;
return std::nullopt;
return create_fallback_entity();
}
std::vector<Rc<Material>> loaded_materials;
@@ -58,16 +68,11 @@ Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader)
mat->specular_color = glm::vec3{mtl.specular[0], mtl.specular[1], mtl.specular[2]};
if (!mtl.diffuse_texname.empty()) {
mat->diffuse_map = Texture::create(base_dir + mtl.diffuse_texname);
if (!mat->diffuse_map) {
Log::warn() << "Could not create diffuse texture " << mtl.diffuse_texname;
}
mat->diffuse_map = ResourceCache::load_texture(base_dir + mtl.diffuse_texname);
}
if (!mtl.specular_texname.empty()) {
mat->specular_map = Texture::create(base_dir + mtl.specular_texname);
if (!mat->specular_map) {
Log::warn() << "Could not create specular texture" << mtl.specular_texname;
}
// Optional: without it the shader uses specular_color alone
mat->specular_map = ResourceCache::load_texture(base_dir + mtl.specular_texname, false);
}
loaded_materials.push_back(mat);
@@ -111,10 +116,13 @@ Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader)
};
}
// Textures are already flipped on load (stbi_set_flip_vertically_on_load),
// which matches the OBJ convention of a bottom-left UV origin
if (idx.texcoord_index >= 0) {
float v = attrib.texcoords[2 * idx.texcoord_index + 1];
vertex.texture_coords = {
attrib.texcoords[2 * idx.texcoord_index + 0],
1.0f - attrib.texcoords[2 * idx.texcoord_index + 1]
flip_uvs ? 1.0f - v : v
};
}
@@ -151,7 +159,7 @@ Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader)
auto mesh = Mesh::create(vertices, all_indices, submeshes);
if (!mesh) {
Log::error() << "Could not create mesh for " << file_path;
return std::nullopt;
return create_fallback_entity();
}
Log::info() << "Loaded " << file_path << " ("

View File

@@ -9,7 +9,11 @@
namespace Fan
{
Opt<Entity> load_obj(const std::string& file_path, Rc<Shader> shader);
// Never fails: a model that can't be loaded is replaced by a fallback cube.
// flip_uvs: for models authored with a top-left UV origin instead of the OBJ
// standard bottom-left (e.g. the LearnOpenGL backpack)
Entity load_obj(const std::string& file_path, Rc<Shader> shader,
bool flip_uvs = false);
} // namespace Fan

View File

@@ -19,6 +19,8 @@ enum class LogLevel
Off,
};
// Prints one line per statement: the header on construction, the newline when
// the temporary is destroyed at the end of the full expression
class LogStream
{
public:
@@ -70,7 +72,7 @@ private:
class Log
{
public:
static inline LogLevel level = LogLevel::Trace;
static inline LogLevel level = LogLevel::Trace; // Messages below this level are dropped
static void set_level(LogLevel new_level) { level = new_level; }

View File

@@ -1,3 +1,6 @@
#include <algorithm>
#include <array>
#include <cfloat>
#include <cstdio>
#include <random>
#include <vector>
@@ -11,6 +14,34 @@
using namespace Fan;
// Ring buffer of per-frame measurements, for plotting and stable averages
struct MetricHistory
{
static constexpr size_t SIZE = 120;
std::array<float, SIZE> values{};
size_t offset{}; // Index of the oldest value, where the next one is written
size_t count{};
void push(float value)
{
values[offset] = value;
offset = (offset + 1) % SIZE;
count = std::min(count + 1, SIZE);
}
float average() const
{
if (count == 0) { return 0.0f; }
float sum = 0.0f;
for (size_t i = 0; i < count; ++i) {
sum += values[(offset + SIZE - 1 - i) % SIZE];
}
return sum / static_cast<float>(count);
}
};
struct UserData
{
Window& window;
@@ -27,6 +58,15 @@ struct UserData
bool vsync{true};
bool show_debug{};
Rc<Texture> logo{};
// Performance measurements of the scene pass (GPU timer is null if unsupported)
CpuTimer scene_cpu_timer{};
Rc<GpuTimer> scene_gpu_timer{};
MetricHistory frame_times{};
MetricHistory scene_cpu_times{};
MetricHistory scene_gpu_times{};
std::vector<Rc<Scene>> scenes{};
size_t selected_scene_index{};
};
@@ -36,10 +76,10 @@ void mouse_callback(double pos_x, double pos_y, void* user_data);
void setup_dockspace();
void draw_panel_window(UserData& user_data);
void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data);
void plot_metric(const char* label, const MetricHistory& history);
void add_random_lights(Scene& scene, int point_light_count, int spot_light_count,
const glm::vec3& box_min, const glm::vec3& box_max,
unsigned int seed = std::random_device{}());
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)
@@ -63,7 +103,7 @@ int main()
if (!window) return EXIT_FAILURE;
window->set_vsync(true);
Renderer::init();
if (!Renderer::init()) return EXIT_FAILURE;
Camera camera{{6.0f, 3.0f, 1.0f}};
camera.fov = 60.0f;
@@ -75,28 +115,25 @@ int main()
/******************** 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", false);
auto shader = ResourceCache::load_shader("./src/shaders/default.vert", "./src/shaders/default.frag");
auto debug_shader = ResourceCache::load_shader("./src/shaders/debug.vert", "./src/shaders/debug.frag", false);
auto sponza = load_obj("./assets/sponza/sponza.obj", shader);
Entity sponza = load_obj("./assets/sponza/sponza.obj", shader);
Entity car = load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader);
Entity backpack = load_obj("./assets/backpack/backpack.obj", shader, true);
auto car = load_obj("./assets/audi_r8/Audi_R8_2017.obj", shader);
auto cube_texture = ResourceCache::load_texture("./assets/textures/container.jpg");
auto default_texture = ResourceCache::load_texture("./assets/textures/wall.jpg");
user_data.logo = ResourceCache::load_texture("assets/logo/logo.png");
auto backpack = load_obj("./assets/backpack/backpack.obj", shader);
auto cube_mesh = Mesh::create(get_cube_vertices());
auto cube_texture = Texture::create("./assets/textures/container.jpg");
auto default_mesh = Mesh::create(get_cube_vertices());
auto default_texture = Texture::create("./assets/textures/debugempty.png");
user_data.scene_gpu_timer = GpuTimer::create();
auto cube_mesh = Mesh::create_cube();
auto framebuffer = FrameBuffer::create(window->get_width(), window->get_height());
if (!shader || !debug_shader
|| !backpack
|| !cube_mesh || !cube_texture
|| !default_mesh || !default_texture
|| !framebuffer) {
// Built from memory or sized from the window, failing here means OpenGL is broken
if (!cube_mesh || !framebuffer) {
Renderer::shutdown();
return EXIT_FAILURE;
}
@@ -106,9 +143,9 @@ int main()
Rc<Scene> sponza_scene = make_rc<Scene>();
user_data.scenes.push_back(sponza_scene);
sponza->transform.position = {0.0f, -0.5f, 0.0f};
sponza->transform.scale = {0.008f, 0.008f, 0.008f};
sponza_scene->add_entity(*sponza);
sponza.transform.position = {0.0f, -0.5f, 0.0f};
sponza.transform.scale = {0.008f, 0.008f, 0.008f};
sponza_scene->add_entity(sponza);
add_random_lights(*sponza_scene, 4, 4,
glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f),
@@ -118,7 +155,7 @@ int main()
Rc<Scene> car_scene = make_rc<Scene>();
user_data.scenes.push_back(car_scene);
car_scene->add_entity(*car);
car_scene->add_entity(car);
add_random_lights(*car_scene, 4, 4,
glm::vec3(-10.0f, 1.0f, -3.0f), glm::vec3(10.0f, 5.0f, 3.0f),
@@ -128,7 +165,7 @@ int main()
Rc<Scene> backpack_scene = make_rc<Scene>();
user_data.scenes.push_back(backpack_scene);
backpack_scene->add_entity(*backpack);
backpack_scene->add_entity(backpack);
auto default_material = make_rc<Material>();
default_material->shader = shader;
@@ -175,6 +212,7 @@ int main()
window->update();
user_data.delta_time = window->get_delta_time();
user_data.frame_times.push(user_data.delta_time * 1000.0f);
process_input(*window, camera, user_data);
Renderer::set_clear_color({0.0f, 0.0f, 0.0f, 1.0f});
@@ -191,18 +229,30 @@ int main()
int width = framebuffer->get_width();
int height = framebuffer->get_height();
framebuffer->bind();
user_data.scene_cpu_timer.begin();
if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->begin(); }
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);
debug_shader, ResourceCache::get_fallback_mesh());
}
if (user_data.scene_gpu_timer) { user_data.scene_gpu_timer->end(); }
user_data.scene_cpu_timer.end();
framebuffer->unbind();
user_data.scene_cpu_times.push(user_data.scene_cpu_timer.get_elapsed_ms());
if (user_data.scene_gpu_timer) {
user_data.scene_gpu_times.push(user_data.scene_gpu_timer->get_elapsed_ms());
}
window->end();
}
Renderer::shutdown();
return EXIT_SUCCESS;
}
@@ -281,11 +331,9 @@ void setup_dockspace()
void draw_panel_window(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}, {0.0f, 1.0f}, {1.0f, 0.0f});
if (user_data.logo) ImGui::Image(user_data.logo->get_id(), {50.0f, 50.0f}, {0.0f, 1.0f}, {1.0f, 0.0f});
ImGui::SameLine();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (50.0f - ImGui::GetTextLineHeight()) * 0.5f);
ImGui::Text("Fan Engine");
@@ -312,6 +360,17 @@ void draw_panel_window(UserData& user_data)
ImGui::DragFloat3("Position", &user_data.camera.position.x, 0.1f, -20.0f, 20.0f, "%.1f");
ImGui::SliderFloat("FOV", &user_data.camera.fov, 30.0f, 100.0f, "%.1f");
ImGui::SeparatorText("Performance");
ImGui::TextDisabled("Disable VSync for accurate frame times");
plot_metric("Frame", user_data.frame_times);
plot_metric("Scene CPU", user_data.scene_cpu_times);
if (user_data.scene_gpu_timer) {
plot_metric("Scene GPU", user_data.scene_gpu_times);
} else {
ImGui::TextDisabled("Scene GPU: timer queries not supported");
}
ImGui::SeparatorText("Key Bindings");
ImGui::Text("ESCAPE: Quit application");
@@ -330,23 +389,44 @@ void draw_viewport_window(FrameBuffer& framebuffer, UserData& user_data)
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);
// Size the framebuffer in physical pixels so HiDPI displays render at full resolution
ImVec2 scale = ImGui::GetIO().DisplayFramebufferScale;
GLsizei fb_width = static_cast<GLsizei>(size.x * scale.x);
GLsizei fb_height = static_cast<GLsizei>(size.y * scale.y);
if (fb_width != framebuffer.get_width() || fb_height != framebuffer.get_height()) {
framebuffer.resize(fb_width, fb_height);
}
ImGui::Image(framebuffer.get_texture_id(), size, ImVec2{0.0f, 1.0f}, ImVec2{1.0f, 0.0f});
}
// Overlays
float fps = user_data.window.get_delta_time() > 0.0f ? 1.0f / user_data.window.get_delta_time() : 0.0f;
// Overlays (averaged over the last MetricHistory::SIZE frames so they're readable)
float frame_ms = user_data.frame_times.average();
float fps = frame_ms > 0.0f ? 1000.0f / frame_ms : 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", user_data.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"); }
overlay_text(20.0f, 69.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Frame time: %.2f ms", frame_ms);
overlay_text(20.0f, 86.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene CPU: %.2f ms", user_data.scene_cpu_times.average());
if (user_data.scene_gpu_timer) {
overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "Scene GPU: %.2f ms", user_data.scene_gpu_times.average());
} else {
overlay_text(20.0f, 103.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "Scene GPU: n/a");
}
if (user_data.mouse_grab) { overlay_text(20.0f, 120.0f, {0.0f, 1.0f, 0.0f, 1.0f}, "%s", "INPUT LOCKED"); }
ImGui::End();
ImGui::PopStyleVar();
}
void plot_metric(const char* label, const MetricHistory& history)
{
char overlay[32];
std::snprintf(overlay, sizeof(overlay), "avg %.2f ms", history.average());
// Values are stored as a ring buffer, values_offset makes the plot start at the oldest
ImGui::PlotLines(label, history.values.data(), static_cast<int>(history.values.size()),
static_cast<int>(history.offset), overlay, 0.0f, FLT_MAX, ImVec2{0.0f, 40.0f});
}
void add_random_lights(Scene& scene, int point_light_count, int spot_light_count,
const glm::vec3& box_min, const glm::vec3& box_max,
unsigned int seed/* = std::random_device{}()*/)
@@ -417,51 +497,3 @@ void add_random_lights(Scene& scene, int point_light_count, int spot_light_count
scene.light_environment.spot_lights.push_back(light);
}
}
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}}
};
}

View File

@@ -10,13 +10,16 @@ namespace Fan
struct Material
{
Rc<Shader> shader;
Rc<Texture> diffuse_map;
Rc<Texture> specular_map;
Rc<Texture> diffuse_map; // Optional
Rc<Texture> specular_map; // Optional
// Multiplied with the maps when present, used alone otherwise
glm::vec3 diffuse_color{1.0f, 1.0f, 1.0f};
glm::vec3 specular_color{1.0f, 1.0f, 1.0f};
float shininess{32.0f};
// Binds the shader and sets the u_material uniforms. Uses texture units
// 0 (diffuse) and 1 (specular)
void bind() const;
};

View File

@@ -2,6 +2,7 @@
#include <utility>
#include "mesh.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -11,9 +12,34 @@ Rc<Mesh> Mesh::create(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices/* = {}*/,
const std::vector<SubMesh>& submeshes/* = {}*/)
{
GLuint VBO{}, VAO{}, EBO{};
bool has_indices = !indices.empty();
// Validate inputs: out of range indices make the GPU read past the buffer
if (vertices.empty()) {
Log::error() << "Could not create mesh: no vertices";
return nullptr;
}
for (GLuint index : indices) {
if (index >= vertices.size()) {
Log::error() << "Could not create mesh: index " << index
<< " out of range (" << vertices.size() << " vertices)";
return nullptr;
}
}
size_t element_count = has_indices ? indices.size() : vertices.size();
for (const auto& submesh : submeshes) {
if (static_cast<size_t>(submesh.index_offset) + submesh.index_count > element_count) {
Log::error() << "Could not create mesh: submesh range (offset " << submesh.index_offset
<< ", count " << submesh.index_count << ") exceeds " << element_count;
return nullptr;
}
}
gl_clear_errors();
GLuint VBO{}, VAO{}, EBO{};
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
@@ -21,14 +47,14 @@ Rc<Mesh> Mesh::create(const std::vector<Vertex>& vertices,
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]),
&vertices[0], GL_DYNAMIC_DRAW);
vertices.data(), GL_DYNAMIC_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);
indices.data(), GL_STATIC_DRAW);
}
// Define vertex data layout
@@ -40,6 +66,19 @@ Rc<Mesh> Mesh::create(const std::vector<Vertex>& vertices,
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
// Unbind so later GL_ELEMENT_ARRAY_BUFFER binds can't modify this VAO
glBindVertexArray(0);
// Buffer uploads can fail with GL_OUT_OF_MEMORY on large meshes
GLenum err = gl_pop_error();
if (err != GL_NO_ERROR) {
Log::error() << "Could not upload mesh data: " << gl_error_string(err);
glDeleteBuffers(1, &EBO);
glDeleteBuffers(1, &VBO);
glDeleteVertexArrays(1, &VAO);
return nullptr;
}
// Submesh setup
std::vector<SubMesh> final_submeshes = submeshes;
@@ -55,6 +94,56 @@ Rc<Mesh> Mesh::create(const std::vector<Vertex>& vertices,
return make_rc<Mesh>(vertices, indices, final_submeshes, has_indices, VBO, VAO, EBO);
}
Rc<Mesh> Mesh::create_cube()
{
const std::vector<Vertex> vertices{
// 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}}
};
return create(vertices);
}
Mesh::~Mesh()
{
glDeleteBuffers(1, &m_EBO);

View File

@@ -14,6 +14,8 @@ namespace Fan
class Mesh
{
public:
// A range drawn with one material. Offset and count are in indices, or in
// vertices if the mesh has no index buffer
struct SubMesh
{
GLuint index_offset{};
@@ -21,9 +23,13 @@ public:
Rc<Material> material{};
};
// Without indices the vertices are drawn in order. Without submeshes a single
// one with no material covers the whole mesh
static Rc<Mesh> create(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices = {},
const std::vector<SubMesh>& submeshes = {});
// Unit cube centered at the origin, with normals and texture coordinates
static Rc<Mesh> create_cube();
Mesh(const std::vector<Vertex>& vertices,
const std::vector<GLuint>& indices,
const std::vector<SubMesh>& submeshes,
@@ -45,6 +51,7 @@ public:
const std::vector<SubMesh>& get_submeshes() const;
// Geometry only: the caller binds the shader and material first
void draw() const;
void draw_submesh(GLuint index_offset, GLuint index_count) const;

View File

@@ -1,9 +1,9 @@
#include <algorithm>
#include <string>
#include <unordered_map>
#include <glad/gl.h>
#include "renderer.hpp"
#include "resourcecache.hpp"
#include "logger.hpp"
namespace Fan
@@ -12,7 +12,7 @@ namespace Fan
Rc<UniformBuffer> Renderer::s_camera_ubo = nullptr;
Rc<UniformBuffer> Renderer::s_light_ubo = nullptr;
void Renderer::init()
bool Renderer::init()
{
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
@@ -22,6 +22,22 @@ void Renderer::init()
using bp = Shader::BindingPoint;
s_camera_ubo = UniformBuffer::create(sizeof(CameraData), bp::Camera);
s_light_ubo = UniformBuffer::create(sizeof(LightEnvironmentData), bp::LightEnvironment);
if (!s_camera_ubo || !s_light_ubo || !ResourceCache::init()) {
Log::error() << "Could not initialize renderer";
shutdown();
return false;
}
return true;
}
void Renderer::shutdown()
{
// Release GPU resources while the GL context is still alive
ResourceCache::shutdown();
s_camera_ubo.reset();
s_light_ubo.reset();
}
void Renderer::set_viewport(int x, int y, int width, int height)
@@ -61,40 +77,27 @@ void Renderer::draw_scene(const Scene& scene, const Camera& camera,
using RenderQueue = std::unordered_map<Shader*, std::unordered_map<Material*, std::vector<DrawCommand>>>;
RenderQueue render_queue;
Material* fallback_material = ResourceCache::get_fallback_material().get();
for (const auto& entity : scene.get_entities()) {
if (!entity.mesh) { continue; }
// If Entity has an explicit material override
if (entity.material) {
if (!entity.material->shader) { continue; }
for (const auto& submesh : entity.mesh->get_submeshes()) {
// An explicit Entity material overrides the SubMesh's native one
Material* material_ptr = entity.material ? entity.material.get()
: submesh.material.get();
Shader* shader_ptr = entity.material->shader.get();
Material* material_ptr = entity.material.get();
for (const auto& submesh : entity.mesh->get_submeshes()) {
render_queue[shader_ptr][material_ptr].push_back({
&entity.transform,
entity.mesh.get(),
submesh.index_offset,
submesh.index_count
});
// Draw broken materials with the fallback instead of skipping them
if (!material_ptr || !material_ptr->shader) {
material_ptr = fallback_material;
}
// Otherwise use SubMesh's native material
} else {
for (const auto& submesh : entity.mesh->get_submeshes()) {
if (!submesh.material || !submesh.material->shader) { continue; }
Shader* shader_ptr = submesh.material->shader.get();
Material* material_ptr = submesh.material.get();
render_queue[shader_ptr][material_ptr].push_back({
&entity.transform,
entity.mesh.get(),
submesh.index_offset,
submesh.index_count
});
}
render_queue[material_ptr->shader.get()][material_ptr].push_back({
&entity.transform,
entity.mesh.get(),
submesh.index_offset,
submesh.index_count
});
}
}
@@ -106,7 +109,12 @@ void Renderer::draw_scene(const Scene& scene, const Camera& camera,
material->bind();
for (const auto& cmd: draw_command) {
shader->set_mat4("u_model", cmd.transform->get_model_matrix());
glm::mat4 model = cmd.transform->get_model_matrix();
shader->set_mat4("u_model", model);
// Inverse-transpose keeps normals perpendicular under non-uniform scale
shader->set_mat3("u_normal_matrix", glm::transpose(glm::inverse(glm::mat3(model))));
cmd.mesh->draw_submesh(cmd.index_offset, cmd.index_count);
}
}
@@ -157,12 +165,14 @@ void Renderer::draw_debug_wireframes(const Scene& scene, const Camera& camera,
set_wireframe(false);
}
void Renderer::update_global_data(const Scene& scene, const Camera& camera, int screen_width, int screen_height)
void Renderer::update_global_data(const Scene& scene, const Camera& camera,
int screen_width, int screen_height)
{
// Update camera UBO
CameraData cam_data{};
cam_data.view = camera.get_view_matrix();
cam_data.projection = camera.get_projection_matrix(screen_width, screen_height);
cam_data.position = glm::vec4(camera.position, 1.0f);
s_camera_ubo->update(&cam_data, sizeof(CameraData));
// Update light UBO
@@ -177,7 +187,7 @@ void Renderer::update_global_data(const Scene& scene, const Camera& camera, int
light_data.dir_light.specular = glm::vec4(dir_src.specular, 0.0f);
// Map PointLights
if (light_data.active_point_lights > MAX_POINT_LIGHTS) {
if (env.point_lights.size() > MAX_POINT_LIGHTS) {
Log::warn() << "Maximum number of point lights reached (" << MAX_POINT_LIGHTS << ")";
}
light_data.active_point_lights = std::min(static_cast<int>(env.point_lights.size()), MAX_POINT_LIGHTS);
@@ -192,7 +202,7 @@ void Renderer::update_global_data(const Scene& scene, const Camera& camera, int
}
// Map SpotLights
if (light_data.active_spot_lights > MAX_SPOT_LIGHTS) {
if (env.spot_lights.size() > MAX_SPOT_LIGHTS) {
Log::warn() << "Maximum number of spot lights reached (" << MAX_SPOT_LIGHTS << ")";
}
light_data.active_spot_lights = std::min(static_cast<int>(env.spot_lights.size()), MAX_SPOT_LIGHTS);

View File

@@ -11,15 +11,24 @@ namespace Fan
class Renderer
{
public:
static void init();
// init() needs a current GL context and also initializes the ResourceCache.
// shutdown() must run before the window (and its context) is destroyed
static bool init();
static void shutdown();
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 set_wireframe(bool enabled); // Global, stays on until disabled
static void clear();
// Draws into the currently bound framebuffer without clearing it.
// screen_width and screen_height only set the camera aspect ratio
static void draw_scene(const Scene& scene, const Camera& camera,
int screen_width, int screen_height);
// debug_shader needs u_model, u_view_projection and u_color. default_mesh is
// drawn for entities without a mesh and for lights. Disables wireframe when done
static void draw_debug_wireframes(const Scene& scene, const Camera& camera,
int screen_width, int screen_height,
const Rc<Shader>& debug_shader,
@@ -35,6 +44,7 @@ private:
struct CameraData {
glm::mat4 view;
glm::mat4 projection;
glm::vec4 position; // w unused, std140 pads vec3 to 16 bytes anyway
};
// Maps to Fan::DirectionalLight
@@ -75,7 +85,6 @@ private:
PointLightData point_lights[MAX_POINT_LIGHTS];
SpotLightData spot_lights[MAX_SPOT_LIGHTS];
};
};
} // namespace Fan

189
src/resourcecache.cpp Normal file
View File

@@ -0,0 +1,189 @@
#include <vector>
#include "resourcecache.hpp"
#include "logger.hpp"
namespace Fan
{
std::unordered_map<std::string, Weak<Texture>> ResourceCache::s_textures{};
std::unordered_map<std::string, Weak<Shader>> ResourceCache::s_shaders{};
std::unordered_set<std::string> ResourceCache::s_failed_textures{};
Rc<Texture> ResourceCache::s_fallback_texture = nullptr;
Rc<Shader> ResourceCache::s_fallback_shader = nullptr;
Rc<Material> ResourceCache::s_fallback_material = nullptr;
Rc<Mesh> ResourceCache::s_fallback_mesh = nullptr;
// Unlit, shows the material's diffuse map (the checkerboard on the fallback material)
static constexpr const char* FALLBACK_VERTEX_SOURCE = R"(#version 330 core
layout (location = 0) in vec3 a_position;
layout (location = 2) in vec2 a_texture_coords;
out vec2 texture_coords;
layout (std140) uniform CameraBlock {
mat4 view;
mat4 projection;
vec4 camera_position; // w unused
};
uniform mat4 u_model;
void main()
{
gl_Position = projection * view * u_model * vec4(a_position, 1.0);
texture_coords = a_texture_coords;
}
)";
static constexpr const char* FALLBACK_FRAGMENT_SOURCE = R"(#version 330 core
struct Material {
sampler2D diffuse;
};
uniform Material u_material;
in vec2 texture_coords;
out vec4 frag_color;
void main()
{
frag_color = vec4(texture(u_material.diffuse, texture_coords).rgb, 1.0);
}
)";
static Rc<Texture> create_checker_texture()
{
constexpr int SIZE = 64;
constexpr int CELL_SIZE = 8;
std::vector<unsigned char> pixels(SIZE * SIZE * 4);
for (int y = 0; y < SIZE; ++y) {
for (int x = 0; x < SIZE; ++x) {
bool magenta = ((x / CELL_SIZE) + (y / CELL_SIZE)) % 2 == 0;
unsigned char* pixel = &pixels[(y * SIZE + x) * 4];
pixel[0] = magenta ? 255 : 0;
pixel[1] = 0;
pixel[2] = magenta ? 255 : 0;
pixel[3] = 255;
}
}
return Texture::create_from_pixels(SIZE, SIZE, pixels.data(), "Fallback checkerboard");
}
bool ResourceCache::init()
{
s_fallback_texture = create_checker_texture();
s_fallback_mesh = Mesh::create_cube();
// Only the camera block is needed, binding the standard ones would log
// an error for the missing LightEnvironmentBlock
s_fallback_shader = Shader::create_from_source(FALLBACK_VERTEX_SOURCE,
FALLBACK_FRAGMENT_SOURCE,
"Fallback shader", false);
if (s_fallback_shader) {
s_fallback_shader->bind_uniform_block("CameraBlock", Shader::BindingPoint::Camera);
}
// These are built from memory, so failing here means OpenGL itself is broken
if (!s_fallback_texture || !s_fallback_shader || !s_fallback_mesh) {
Log::error() << "Could not create fallback resources";
shutdown();
return false;
}
s_fallback_material = make_rc<Material>();
s_fallback_material->shader = s_fallback_shader;
s_fallback_material->diffuse_map = s_fallback_texture;
return true;
}
void ResourceCache::shutdown()
{
// Release GPU resources while the GL context is still alive
s_textures.clear();
s_shaders.clear();
s_failed_textures.clear();
s_fallback_material.reset();
s_fallback_texture.reset();
s_fallback_shader.reset();
s_fallback_mesh.reset();
}
Rc<Texture> ResourceCache::load_texture(const std::string& path, bool use_fallback/* = true*/)
{
if (s_failed_textures.count(path) > 0) {
return use_fallback ? s_fallback_texture : nullptr;
}
Weak<Texture>& entry = s_textures[path];
if (auto cached = entry.lock()) {
return cached;
}
Rc<Texture> texture = Texture::create(path);
if (!texture) {
s_failed_textures.insert(path);
if (!use_fallback) {
return nullptr;
}
Log::warn() << "Using fallback texture for " << path;
return s_fallback_texture;
}
entry = texture;
return texture;
}
Rc<Shader> ResourceCache::load_shader(const std::string& vertex_path,
const std::string& fragment_path,
bool bind_standard_blocks/* = true*/)
{
std::string key = vertex_path + "|" + fragment_path + (bind_standard_blocks ? "|std" : "");
Weak<Shader>& entry = s_shaders[key];
if (auto cached = entry.lock()) {
return cached;
}
Rc<Shader> shader = Shader::create(vertex_path, fragment_path, bind_standard_blocks);
if (!shader) {
Log::warn() << "Using fallback shader for (" << vertex_path << ", " << fragment_path << ")";
shader = s_fallback_shader;
}
entry = shader;
return shader;
}
const Rc<Texture>& ResourceCache::get_fallback_texture()
{
return s_fallback_texture;
}
const Rc<Shader>& ResourceCache::get_fallback_shader()
{
return s_fallback_shader;
}
const Rc<Material>& ResourceCache::get_fallback_material()
{
return s_fallback_material;
}
const Rc<Mesh>& ResourceCache::get_fallback_mesh()
{
return s_fallback_mesh;
}
} // namespace Fan

56
src/resourcecache.hpp Normal file
View File

@@ -0,0 +1,56 @@
#ifndef FAN_RESOURCECACHE_HPP_
#define FAN_RESOURCECACHE_HPP_
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "material.hpp"
#include "mesh.hpp"
#include "shader.hpp"
#include "texture.hpp"
#include "utils.hpp"
namespace Fan
{
// Shares loaded resources and substitutes fallbacks for the ones that fail to
// load, so a broken asset shows up as a magenta checkerboard instead of a
// crash. Initialized and shut down by the Renderer.
class ResourceCache
{
public:
static bool init();
static void shutdown();
// Never return null after init(). A failed load is logged once and the
// fallback is returned for that path from then on.
// use_fallback: false for optional textures (e.g. specular maps), which
// return null on failure so the material can do without them
static Rc<Texture> load_texture(const std::string& path, bool use_fallback = true);
static Rc<Shader> load_shader(const std::string& vertex_path,
const std::string& fragment_path,
bool bind_standard_blocks = true);
static const Rc<Texture>& get_fallback_texture();
static const Rc<Shader>& get_fallback_shader();
static const Rc<Material>& get_fallback_material();
static const Rc<Mesh>& get_fallback_mesh();
private:
// Weak references: the cache doesn't keep unused resources alive
static std::unordered_map<std::string, Weak<Texture>> s_textures;
static std::unordered_map<std::string, Weak<Shader>> s_shaders;
// Paths that failed to load, so they aren't retried (and re-logged)
static std::unordered_set<std::string> s_failed_textures;
static Rc<Texture> s_fallback_texture;
static Rc<Shader> s_fallback_shader;
static Rc<Material> s_fallback_material;
static Rc<Mesh> s_fallback_mesh;
};
} // namespace Fan
#endif // FAN_RESOURCECACHE_HPP_

View File

@@ -14,6 +14,8 @@ class Scene
public:
LightEnvironment light_environment{};
// Stores a copy: later changes to the passed Entity don't affect the scene.
// Meshes and materials are shared, not copied
void add_entity(const Entity& entity);
const std::vector<Entity>& get_entities() const;

View File

@@ -3,6 +3,7 @@
#include "shader.hpp"
#include "utils.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -12,18 +13,47 @@ Rc<Shader> Shader::create(const std::string& vertex_shader_path,
const std::string& fragment_shader_path,
bool bind_standard_blocks/* = true*/)
{
auto vertex_shader = compile_shader(vertex_shader_path, GL_VERTEX_SHADER);
auto vertex_source = read_entire_file(vertex_shader_path);
if (!vertex_source) {
Log::error() << "Could not read shader " << vertex_shader_path;
return nullptr;
}
auto fragment_source = read_entire_file(fragment_shader_path);
if (!fragment_source) {
Log::error() << "Could not read shader " << fragment_shader_path;
return nullptr;
}
return create_from_source(*vertex_source, *fragment_source,
vertex_shader_path + ", " + fragment_shader_path,
bind_standard_blocks);
}
Rc<Shader> Shader::create_from_source(const std::string& vertex_source,
const std::string& fragment_source,
const std::string& name,
bool bind_standard_blocks/* = true*/)
{
auto vertex_shader = compile_shader(vertex_source, GL_VERTEX_SHADER, name);
if (!vertex_shader) {
return nullptr;
}
auto fragment_shader = compile_shader(fragment_shader_path, GL_FRAGMENT_SHADER);
auto fragment_shader = compile_shader(fragment_source, GL_FRAGMENT_SHADER, name);
if (!fragment_shader) {
glDeleteShader(*vertex_shader);
return nullptr;
}
GLuint shader_program = glCreateProgram();
if (shader_program == 0) {
Log::error() << "glCreateProgram failed for " << name;
glDeleteShader(*vertex_shader);
glDeleteShader(*fragment_shader);
return nullptr;
}
glAttachShader(shader_program, *vertex_shader);
glAttachShader(shader_program, *fragment_shader);
glLinkProgram(shader_program);
@@ -40,22 +70,23 @@ Rc<Shader> Shader::create(const std::string& vertex_shader_path,
if (log_length > 0) {
std::string log(log_length, ' ');
glGetProgramInfoLog(shader_program, log_length, nullptr, &log[0]);
Log::error() << "Could not link shader program: " << log;
Log::error() << "Could not link shader program (" << name << "): " << log;
}
glDeleteProgram(shader_program);
return nullptr;
}
Rc<Shader> shader = make_rc<Shader>(shader_program);
set_gl_label(GL_PROGRAM, shader_program, name);
Rc<Shader> shader = make_rc<Shader>(shader_program, name);
if (bind_standard_blocks) {
shader->bind_uniform_block("CameraBlock", BindingPoint::Camera);
shader->bind_uniform_block("LightEnvironmentBlock", BindingPoint::LightEnvironment);
}
Log::info() << "Created shader (" << vertex_shader_path << ", "
<< fragment_shader_path << ")";
Log::info() << "Created shader (" << name << ")";
return shader;
}
@@ -66,6 +97,8 @@ Shader::~Shader()
Shader::Shader(Shader&& other) noexcept
: m_id(std::exchange(other.m_id, 0))
, m_name(std::move(other.m_name))
, m_uniform_locations(std::move(other.m_uniform_locations))
{
}
@@ -74,6 +107,8 @@ Shader& Shader::operator=(Shader&& other) noexcept
if (this == &other) { return *this; }
std::swap(m_id, other.m_id);
std::swap(m_name, other.m_name);
std::swap(m_uniform_locations, other.m_uniform_locations);
return *this;
}
@@ -83,52 +118,75 @@ GLuint Shader::get_id() const
return m_id;
}
GLint Shader::get_location(const std::string &name)
const std::string& Shader::get_name() const
{
GLint location = glGetUniformLocation(m_id, name.c_str());
if (location == -1) {
Log::warn() << "Uniform " << name << " not found";
return m_name;
}
GLint Shader::get_location(std::string_view name)
{
auto it = m_uniform_locations.find(name);
if (it != m_uniform_locations.end()) {
return it->second;
}
// First lookup of this name: ask the driver and remember the result,
// including misses so the warning is only printed once
std::string name_str{name};
GLint location = glGetUniformLocation(m_id, name_str.c_str());
if (location == -1) {
Log::warn() << "Uniform " << name_str << " not found in shader (" << m_name << ")";
}
m_uniform_locations.emplace(std::move(name_str), location);
return location;
}
void Shader::set_int(const std::string& name, GLint value)
void Shader::set_int(std::string_view name, GLint value)
{
glUniform1i(get_location(name), value);
}
void Shader::set_float(const std::string& name, float value)
void Shader::set_float(std::string_view name, float value)
{
glUniform1f(get_location(name), value);
}
void Shader::set_vec3(const std::string& name, float x, float y, float z)
void Shader::set_vec3(std::string_view 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)
void Shader::set_vec3(std::string_view 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)
void Shader::set_mat3(std::string_view name, const glm::mat3& value)
{
GLint location = get_location(name);
glUniformMatrix3fv(location, 1, GL_FALSE, glm::value_ptr(value));
}
void Shader::set_mat4(std::string_view 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)
Opt<GLuint> Shader::compile_shader(const std::string& source,
GLenum shader_type,
const std::string& name)
{
auto shader_source = read_entire_file(source_path);
if (!shader_source) {
Log::error() << "Could not read shader " << source_path;
const char* type_name = shader_type == GL_VERTEX_SHADER ? "vertex" : "fragment";
GLuint shader = glCreateShader(shader_type);
if (shader == 0) {
Log::error() << "glCreateShader failed for " << type_name << " shader (" << name << ")";
return std::nullopt;
}
GLuint shader = glCreateShader(shader_type);
const char *shader_source_str = shader_source->c_str();
const char *shader_source_str = source.c_str();
glShaderSource(shader, 1, &shader_source_str, nullptr);
glCompileShader(shader);
@@ -141,7 +199,7 @@ Opt<GLuint> Shader::compile_shader(const std::string& source_path,
if (log_length > 0) {
std::string log(log_length, ' ');
glGetShaderInfoLog(shader, log_length, nullptr, &log[0]);
Log::error() << "Could not compile shader: " << log;
Log::error() << "Could not compile " << type_name << " shader (" << name << "): " << log;
}
glDeleteShader(shader);

View File

@@ -1,7 +1,10 @@
#ifndef FAN_SHADER_HPP_
#define FAN_SHADER_HPP_
#include <functional>
#include <map>
#include <string>
#include <string_view>
#include <glad/gl.h>
#include <glm/glm.hpp>
@@ -13,16 +16,23 @@ namespace Fan
class Shader
{
public:
// Uniform buffer binding points shared by all shaders
struct BindingPoint
{
static constexpr GLuint Camera = 0;
static constexpr GLuint LightEnvironment = 1;
};
// bind_standard_blocks: bind CameraBlock and LightEnvironmentBlock to their
// BindingPoint. Logs an error for each block the shader doesn't declare
static Rc<Shader> create(const std::string& vertex_path,
const std::string& fragment_path,
bool bind_standard_blocks = true);
Shader(GLuint id) : m_id{id} {}
static Rc<Shader> create_from_source(const std::string& vertex_source,
const std::string& fragment_source,
const std::string& name,
bool bind_standard_blocks = true);
Shader(GLuint id, const std::string& name) : m_id{id}, m_name{name} {}
~Shader();
Shader(const Shader&) = delete;
@@ -31,23 +41,31 @@ public:
Shader& operator=(Shader&& other) noexcept;
GLuint get_id() const;
GLint get_location(const std::string &name);
const std::string& get_name() const;
GLint get_location(std::string_view name); // -1 if not found (warns once per 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 set_int(std::string_view name, GLint value);
void set_float(std::string_view name, float value);
void set_vec3(std::string_view name, float x, float y, float z);
void set_vec3(std::string_view name, const glm::vec3& value);
void set_mat3(std::string_view name, const glm::mat3& value);
void set_mat4(std::string_view name, const glm::mat4& value);
void bind() const;
void bind_uniform_block(const std::string& name, GLuint binding_point) const;
private:
GLuint m_id{};
std::string m_name{};
static Opt<GLuint> compile_shader(const std::string& source_path,
GLenum shader_type);
// Uniform name -> location (-1 if not found). std::less<> allows lookups
// with a string_view, so cache hits don't allocate a std::string
std::map<std::string, GLint, std::less<>> m_uniform_locations{};
static Opt<GLuint> compile_shader(const std::string& source,
GLenum shader_type,
const std::string& name);
};
} // namespace Fan

View File

@@ -42,6 +42,7 @@ struct SpotLight {
layout (std140) uniform CameraBlock {
mat4 view;
mat4 projection;
vec4 camera_position; // w unused
};
#define MAX_POINT_LIGHTS 32
@@ -63,100 +64,111 @@ in vec2 texture_coords;
out vec4 frag_color;
vec3 get_diffuse() {
if (u_material.use_diffuse_map == 1) {
return vec3(texture(u_material.diffuse, texture_coords)) * u_material.diffuse_color;
}
return u_material.diffuse_color;
}
// Light reaching the surface, grouped by the material color it gets multiplied
// with. Applying the material once at the end saves work per light
struct Lighting {
vec3 diffuse; // ambient + diffuse light, multiplied by the albedo
vec3 specular; // multiplied by the specular color
};
vec3 get_specular() {
if (u_material.use_specular_map == 1) {
return vec3(texture(u_material.specular, texture_coords)) * u_material.specular_color;
}
return u_material.specular_color;
}
vec3 calculate_directional_light(DirLight 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);
vec2 phong(vec3 normal, vec3 light_dir, vec3 view_dir);
void add_directional_light(DirLight light, vec3 normal, vec3 view_dir, inout Lighting lighting);
void add_point_light(PointLight light, vec3 normal, vec3 view_dir, inout Lighting lighting);
void add_spot_light(SpotLight light, vec3 normal, vec3 view_dir, inout Lighting lighting);
void main()
{
vec4 texture_color = texture(u_material.diffuse, texture_coords);
if (texture_color.a < 1e-2) {
discard;
// Sample each texture once, the light functions never touch the material
vec4 diffuse_sample = vec4(1.0);
if (u_material.use_diffuse_map == 1) {
diffuse_sample = texture(u_material.diffuse, texture_coords);
// Discard if diffuse map has a low alpha value
if (diffuse_sample.a < 0.01) {
discard;
}
}
vec3 norm = normalize(normal);
vec3 specular_sample = vec3(1.0);
if (u_material.use_specular_map == 1) {
specular_sample = texture(u_material.specular, texture_coords).rgb;
}
vec3 view_pos = vec3(inverse(view)[3]);
vec3 view_dir = normalize(view_pos - frag_position);
vec3 albedo = diffuse_sample.rgb * u_material.diffuse_color;
vec3 specular_color = specular_sample * u_material.specular_color;
vec3 norm = normalize(normal);
vec3 view_dir = normalize(camera_position.xyz - frag_position);
Lighting lighting = Lighting(vec3(0.0), vec3(0.0));
// DirectionalLight
vec3 result = calculate_directional_light(dir_light, norm, view_dir);
add_directional_light(dir_light, norm, view_dir, lighting);
// PointLights
for (int i = 0; i < active_point_lights; ++i) {
result += calculate_point_light(point_lights[i], norm, frag_position, view_dir);
add_point_light(point_lights[i], norm, view_dir, lighting);
}
// SpotLights
for (int i = 0; i < active_spot_lights; ++i) {
result += calculate_spot_light(spot_lights[i], norm, frag_position, view_dir);
add_spot_light(spot_lights[i], norm, view_dir, lighting);
}
frag_color = vec4(result, 1.0);
frag_color = vec4(lighting.diffuse * albedo + lighting.specular * specular_color, 1.0);
}
vec3 calculate_directional_light(DirLight light, vec3 normal, vec3 view_dir)
// Diffuse (x) and specular (y) factors for a normalized surface-to-light direction
vec2 phong(vec3 normal, vec3 light_dir, vec3 view_dir)
{
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);
return vec2(diff, spec);
}
void add_directional_light(DirLight light, vec3 normal, vec3 view_dir, inout Lighting lighting)
{
vec3 light_dir = normalize(-light.direction.xyz);
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);
vec2 factors = phong(normal, light_dir, view_dir);
vec3 ambient = light.ambient.xyz * get_diffuse();
vec3 diffuse = light.diffuse.xyz * diff * get_diffuse();
vec3 specular = light.specular.xyz * spec * get_specular();
return ambient + diffuse + specular;
lighting.diffuse += light.ambient.xyz + light.diffuse.xyz * factors.x;
lighting.specular += light.specular.xyz * factors.y;
}
vec3 calculate_point_light(PointLight light, vec3 normal, vec3 frag_position, vec3 view_dir)
void add_point_light(PointLight light, vec3 normal, vec3 view_dir, inout Lighting lighting)
{
vec3 light_dir = normalize(light.position.xyz - 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);
// One length() gives both the distance and the normalized direction
vec3 to_light = light.position.xyz - frag_position;
float dist = length(to_light);
vec3 light_dir = to_light / dist;
float dist = length(light.position.xyz - frag_position);
float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist));
vec2 factors = phong(normal, light_dir, view_dir);
vec3 ambient = light.ambient.xyz * get_diffuse();
vec3 diffuse = light.diffuse.xyz * diff * get_diffuse();
vec3 specular = light.specular.xyz * spec * get_specular();
return (ambient + diffuse + specular) * attenuation;
lighting.diffuse += (light.ambient.xyz + light.diffuse.xyz * factors.x) * attenuation;
lighting.specular += light.specular.xyz * factors.y * attenuation;
}
vec3 calculate_spot_light(SpotLight light, vec3 normal, vec3 frag_position, vec3 view_dir)
void add_spot_light(SpotLight light, vec3 normal, vec3 view_dir, inout Lighting lighting)
{
vec3 light_dir = normalize(light.position.xyz - 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.xyz - frag_position);
float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist));
vec3 to_light = light.position.xyz - frag_position;
float dist = length(to_light);
vec3 light_dir = to_light / dist;
// Outside the outer cone the intensity is 0, skip the rest of the work
float theta = dot(light_dir, normalize(-light.direction.xyz));
if (theta <= light.cut_offs.y) {
return;
}
float epsilon = light.cut_offs.x - light.cut_offs.y;
float intensity = clamp((theta - light.cut_offs.y) / epsilon, 0.0, 1.0);
vec3 ambient = light.ambient.xyz * get_diffuse();
vec3 diffuse = light.diffuse.xyz * diff * get_diffuse();
vec3 specular = light.specular.xyz * spec * get_specular();
float attenuation = 1.0 / (light.attenuation.x + light.attenuation.y * dist + light.attenuation.z * (dist * dist));
vec2 factors = phong(normal, light_dir, view_dir);
return (ambient + diffuse + specular) * attenuation * intensity;
lighting.diffuse += (light.ambient.xyz + light.diffuse.xyz * factors.x) * attenuation * intensity;
lighting.specular += light.specular.xyz * factors.y * attenuation * intensity;
}

View File

@@ -11,9 +11,11 @@ out vec2 texture_coords;
layout (std140) uniform CameraBlock {
mat4 view;
mat4 projection;
vec4 camera_position; // w unused
};
uniform mat4 u_model;
uniform mat3 u_normal_matrix; // Inverse-transpose of u_model, computed on the CPU
void main()
{
@@ -21,8 +23,7 @@ void main()
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;
normal = u_normal_matrix * a_normal;
texture_coords = a_texture_coords;
}

View File

@@ -1,6 +1,7 @@
#include <stb_image.h>
#include "texture.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -17,15 +18,30 @@ Rc<Texture> Texture::create(const std::string& image_path)
return nullptr;
}
GLuint texture;
glGenTextures(1, &texture);
if (glGetError() != GL_NO_ERROR) {
Log::error() << "glGenTextures failed";
stbi_image_free(image_data);
auto texture = create_from_pixels(width, height, image_data, image_path);
stbi_image_free(image_data);
return texture;
}
Rc<Texture> Texture::create_from_pixels(GLsizei width, GLsizei height,
const unsigned char* pixels,
const std::string& name)
{
GLint max_size = 0;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_size);
if (width <= 0 || height <= 0 || width > max_size || height > max_size) {
Log::error() << "Texture " << name << " (" << width << "x" << height
<< ") is empty or exceeds the maximum size of " << max_size;
return nullptr;
}
gl_clear_errors();
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
set_gl_label(GL_TEXTURE, texture, name);
// Texture wrapping parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
@@ -35,10 +51,16 @@ Rc<Texture> Texture::create(const std::string& image_path)
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);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
stbi_image_free(image_data);
// Upload can fail with GL_OUT_OF_MEMORY on large images
GLenum err = gl_pop_error();
if (err != GL_NO_ERROR) {
Log::error() << "Could not upload texture " << name << ": " << gl_error_string(err);
glDeleteTextures(1, &texture);
return nullptr;
}
return make_rc<Texture>(texture);
}

View File

@@ -12,7 +12,13 @@ namespace Fan
class Texture
{
public:
// Uploaded as RGBA8 with mipmaps and repeat wrapping. The image is flipped
// vertically so UV (0, 0) is its bottom-left corner
static Rc<Texture> create(const std::string& image_path);
// Pixels are tightly packed RGBA8, first row at the bottom (OpenGL convention)
static Rc<Texture> create_from_pixels(GLsizei width, GLsizei height,
const unsigned char* pixels,
const std::string& name);
Texture(GLuint id) : m_id{id} {}
~Texture();
@@ -23,6 +29,7 @@ public:
GLuint get_id() const;
// slot is the texture unit, which is left as the active unit
void bind(GLuint slot = 0) const;
static void unbind(GLuint slot = 0);

130
src/timer.cpp Normal file
View File

@@ -0,0 +1,130 @@
#include <utility>
#include "timer.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
{
void CpuTimer::begin()
{
m_start = std::chrono::steady_clock::now();
}
void CpuTimer::end()
{
auto elapsed = std::chrono::steady_clock::now() - m_start;
m_elapsed_ms = std::chrono::duration<float, std::milli>(elapsed).count();
}
float CpuTimer::get_elapsed_ms() const
{
return m_elapsed_ms;
}
Rc<GpuTimer> GpuTimer::create()
{
// Drivers report 0 counter bits when timer queries aren't usable
GLint counter_bits = 0;
glGetQueryiv(GL_TIME_ELAPSED, GL_QUERY_COUNTER_BITS, &counter_bits);
if (counter_bits == 0) {
Log::error() << "Could not create GPU timer: timer queries not supported";
return nullptr;
}
gl_clear_errors();
std::array<GLuint, QUERY_COUNT> queries{};
glGenQueries(static_cast<GLsizei>(QUERY_COUNT), queries.data());
GLenum err = gl_pop_error();
if (err != GL_NO_ERROR) {
Log::error() << "Could not create GPU timer: " << gl_error_string(err);
glDeleteQueries(static_cast<GLsizei>(QUERY_COUNT), queries.data());
return nullptr;
}
return make_rc<GpuTimer>(queries);
}
GpuTimer::~GpuTimer()
{
glDeleteQueries(static_cast<GLsizei>(QUERY_COUNT), m_queries.data());
}
GpuTimer::GpuTimer(GpuTimer&& other) noexcept
: m_queries(std::exchange(other.m_queries, {}))
, m_pending(std::exchange(other.m_pending, {}))
, m_current(std::exchange(other.m_current, 0))
, m_elapsed_ms(std::exchange(other.m_elapsed_ms, 0.0f))
, m_active(std::exchange(other.m_active, false))
{
}
GpuTimer& GpuTimer::operator=(GpuTimer&& other) noexcept
{
if (this == &other) { return *this; }
std::swap(m_queries, other.m_queries);
std::swap(m_pending, other.m_pending);
std::swap(m_current, other.m_current);
std::swap(m_elapsed_ms, other.m_elapsed_ms);
std::swap(m_active, other.m_active);
return *this;
}
void GpuTimer::begin()
{
if (m_active) {
Log::warn() << "GpuTimer::begin() called while already active";
return;
}
// Reusing a slot drops its result if the GPU still hasn't delivered it
m_pending[m_current] = false;
glBeginQuery(GL_TIME_ELAPSED, m_queries[m_current]);
m_active = true;
}
void GpuTimer::end()
{
if (!m_active) {
Log::warn() << "GpuTimer::end() called without begin()";
return;
}
glEndQuery(GL_TIME_ELAPSED);
m_active = false;
m_pending[m_current] = true;
m_current = (m_current + 1) % QUERY_COUNT;
collect_results();
}
float GpuTimer::get_elapsed_ms() const
{
return m_elapsed_ms;
}
void GpuTimer::collect_results()
{
// m_current now points to the oldest query. Results become available in
// submission order, so stop at the first one that isn't ready yet
for (size_t i = 0; i < QUERY_COUNT; ++i) {
size_t index = (m_current + i) % QUERY_COUNT;
if (!m_pending[index]) { continue; }
GLint available = GL_FALSE;
glGetQueryObjectiv(m_queries[index], GL_QUERY_RESULT_AVAILABLE, &available);
if (!available) { break; }
GLuint64 elapsed_ns = 0;
glGetQueryObjectui64v(m_queries[index], GL_QUERY_RESULT, &elapsed_ns);
m_elapsed_ms = static_cast<float>(elapsed_ns) / 1'000'000.0f;
m_pending[index] = false;
}
}
} // namespace Fan

64
src/timer.hpp Normal file
View File

@@ -0,0 +1,64 @@
#ifndef FAN_TIMER_HPP_
#define FAN_TIMER_HPP_
#include <array>
#include <chrono>
#include <cstddef>
#include <glad/gl.h>
#include "utils.hpp"
namespace Fan
{
// Measures CPU wall-clock time between begin() and end()
class CpuTimer
{
public:
void begin();
void end();
float get_elapsed_ms() const;
private:
std::chrono::steady_clock::time_point m_start{};
float m_elapsed_ms{};
};
// Measures how long the GPU spends executing the commands between begin() and
// end(). Results are read back a few frames later so the CPU never waits on the
// GPU. GL_TIME_ELAPSED queries can't be nested: only one GpuTimer may be active
class GpuTimer
{
public:
// Frames the GPU may lag behind before a measurement is dropped
static constexpr size_t QUERY_COUNT = 4;
static Rc<GpuTimer> create();
GpuTimer(const std::array<GLuint, QUERY_COUNT>& queries) : m_queries{queries} {}
~GpuTimer();
GpuTimer(const GpuTimer&) = delete;
GpuTimer& operator=(const GpuTimer&) = delete;
GpuTimer(GpuTimer&& other) noexcept;
GpuTimer& operator=(GpuTimer&& other) noexcept;
void begin();
void end();
// Latest available measurement, a few frames old
float get_elapsed_ms() const;
private:
std::array<GLuint, QUERY_COUNT> m_queries{};
std::array<bool, QUERY_COUNT> m_pending{};
size_t m_current{};
float m_elapsed_ms{};
bool m_active{};
void collect_results();
};
} // namespace Fan
#endif // FAN_TIMER_HPP_

View File

@@ -19,16 +19,16 @@ struct Transform
explicit Transform(const glm::vec3& position)
: position(position) {}
// Rotate relative to current orientation
// Rotate around a world-space axis, on top of the current rotation
void rotate(float angle_rad, const glm::vec3& axis);
// Get the model matrix of the transform
// Get the model matrix of the transform (scale, then rotate, then translate)
glm::mat4 get_model_matrix() const;
// Get Euler angles in radians
glm::vec3 get_euler_angles() const;
// Get direction vectors
// Get direction vectors (forward is -Z, OpenGL convention)
glm::vec3 get_forward() const;
glm::vec3 get_right() const;
glm::vec3 get_up() const;

View File

@@ -1,6 +1,7 @@
#include <utility>
#include "uniformbuffer.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -8,6 +9,23 @@ namespace Fan
Rc<UniformBuffer> UniformBuffer::create(size_t size, GLuint binding_point)
{
GLint max_size = 0, max_bindings = 0;
glGetIntegerv(GL_MAX_UNIFORM_BLOCK_SIZE, &max_size);
glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &max_bindings);
if (size == 0 || size > static_cast<size_t>(max_size)) {
Log::error() << "Could not create uniformbuffer: size " << size
<< " outside of (0, " << max_size << "]";
return nullptr;
}
if (binding_point >= static_cast<GLuint>(max_bindings)) {
Log::error() << "Could not create uniformbuffer: binding point " << binding_point
<< " exceeds maximum of " << max_bindings - 1;
return nullptr;
}
gl_clear_errors();
GLuint id;
glGenBuffers(1, &id);
@@ -17,9 +35,9 @@ Rc<UniformBuffer> UniformBuffer::create(size_t size, GLuint binding_point)
glBindBufferBase(GL_UNIFORM_BUFFER, binding_point, id);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
GLenum err = glGetError();
GLenum err = gl_pop_error();
if (err != GL_NO_ERROR) {
Log::error() << "Could not create uniformbuffer";
Log::error() << "Could not create uniformbuffer: " << gl_error_string(err);
glDeleteBuffers(1, &id);
return nullptr;
}

View File

@@ -12,6 +12,8 @@ namespace Fan
class UniformBuffer
{
public:
// The buffer stays bound to binding_point. Shader blocks reach it by being
// bound to the same point (see Shader::bind_uniform_block)
static Rc<UniformBuffer> create(size_t size, GLuint binding_point);
UniformBuffer(GLuint id, size_t size, GLuint binding_point)
: m_id(id)
@@ -28,7 +30,8 @@ public:
size_t get_size() const;
GLuint get_binding_point() const;
// Upload data to the GPU buffer
// Upload data to the GPU buffer. Writes outside the buffer are rejected.
// data must follow the std140 layout of the matching shader block
void update(const void* data, size_t size, size_t offset = 0) const;
private:

View File

@@ -17,6 +17,9 @@ Rc<T> make_rc(Args&&... args)
return std::make_shared<T>(std::forward<Args>(args)...);
}
template<typename T>
using Weak = std::weak_ptr<T>;
template<typename T>
using Opt = std::optional<T>;

View File

@@ -6,6 +6,7 @@
namespace Fan
{
// Shader attribute locations: position 0, normal 1, texture_coords 2
struct Vertex
{
glm::vec3 position;

View File

@@ -5,6 +5,7 @@
#include <imgui_impl_opengl3.h>
#include "window.hpp"
#include "gldebug.hpp"
#include "logger.hpp"
namespace Fan
@@ -23,6 +24,9 @@ Rc<Window> Window::create(int width, int height, const std::string& title)
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifndef NDEBUG
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
#endif
GLFWwindow *window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (window == nullptr) {
@@ -43,6 +47,10 @@ Rc<Window> Window::create(int width, int height, const std::string& title)
return nullptr;
}
#ifndef NDEBUG
enable_gl_debug_output();
#endif
int fb_width, fb_height;
glfwGetFramebufferSize(window, &fb_width, &fb_height);
glViewport(0, 0, fb_width, fb_height);

View File

@@ -10,6 +10,7 @@
namespace Fan
{
// Owns GLFW, the OpenGL context and ImGui, so only one Window may exist
class Window
{
public:
@@ -25,10 +26,15 @@ public:
bool should_close() const;
bool is_valid() const;
bool is_fullscreen() const;
// Key state is sampled in update(). Pressed is only true on the frame the
// key went down
bool is_key_down(int glfw_key);
bool is_key_pressed(int glfw_key);
float get_delta_time();
float get_delta_time(); // Seconds between the last two update() calls
// Window size in screen coordinates, which differ from pixels on HiDPI
// displays. Updated in end()
int get_width() const;
int get_height() const;
@@ -37,12 +43,15 @@ public:
void set_fullscreen(bool enabled);
void set_vsync(bool enabled);
// User callbacks
// User callbacks. Cursor position is in screen coordinates and user_data is
// the pointer passed to set_callback_user_data()
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
// Main loop functions, call once per frame in this order. begin() starts the
// ImGui frame, update() samples time and keys, end() draws ImGui, swaps
// buffers and polls events
void begin();
void update();
void end();