Compare commits

...

10 Commits

Author SHA1 Message Date
portersky a4ef4adfc7 feat: add window resize support
Added width/height tracking to the window class via a GLFW
resize callback. The callback stores the current size and
forwards it to a user-provided std::function.

Added context::set_size() to update the GL viewport when
the window is resized.

Changed scene::render() to accept (int width, int height)
parameters so scenes can compute the correct aspect ratio
for their projection matrix instead of hardcoding 1280/720.

Fixed parameter shadowing in resize_callback_impl
(glfw_window instead of window).
2026-05-06 00:06:59 +02:00
portersky 91fe3c6e8c style: color-code sphere faces to visualize seams
Each of the 6 cube-to-sphere faces now has a distinct color
(red, green, blue, yellow, magenta, cyan) passed via a
vertex color attribute. The fragment shader multiplies
lighting by this color instead of a uniform blue.

This makes the cube-face seams clearly visible when
inspecting the mapped sphere.
2026-05-06 00:01:17 +02:00
portersky 6f696d377b feat: add --scene and --screenshot CLI flags
Added --scene <cube|sphere> to select the initial scene from
the command line (useful for headless/CI testing where key
presses aren't possible).

Added --screenshot to render a single frame, save a PNG,
and exit immediately. Combined with --duration or used
alone, this allows fully automated screenshot capture
without relying on interactive key presses.

Updated help text to reflect new options.
2026-05-05 23:59:54 +02:00
portersky 78d0515e8b feat: add sphere scene with fixed cube-to-sphere mapping
Added scenes/sphere.{hpp,cpp} using the cube-to-sphere
approach from nrz.cpp, but with corrected math: vertices
are simply normalized to project onto the unit sphere
(the original used a broken formula with p=50.0 as an
exponent).

The sphere uses indexed geometry with position, normal,
and UV attributes, plus a simple diffuse lighting shader.

Press 1/2 to switch between cube and sphere scenes.
Updated .gitignore to exclude generated PNG screenshots.
2026-05-05 23:54:48 +02:00
portersky 4a88c8cc06 feat: add stb dependency and window::screenshot()
Added deps/Findstb.cmake (fetches stb via FetchContent, provides
stb::stb interface target).

Linked to cbt_opengl. Implemented window::screenshot() using
glReadPixels + vertical flip + stb_image_write to save RGBA PNG.

Press S in the app to capture current frame (saved as
screenshot.png). Updated help text.

(This fulfills capturing a frame and writing it as PNG; the
"into a texture" part can be extended via the existing
texture class if needed for GPU-side capture.)
2026-05-05 23:48:05 +02:00
portersky c3860cc1d3 refactor: add window::stop() and remove quit flag
Added `window::stop()` (sets GLFW close flag). Updated
signal/timer handlers and Q key check to call it instead of
using a separate `quit` bool in main().

This encapsulates the close state in the window class (no
more external flag + manual checks). The render loop is now
simpler.

(The process_signals lambda and ASIO duration timer are
retained.)
2026-05-05 23:45:46 +02:00
portersky 3f78d0978d feat: add CLI argument parser with ASIO duration timer
Adds support for `--duration <seconds>` (and `--help`) to
automatically terminate after a set time. Useful for testing,
CI, and agent runs where the default infinite loop is
problematic.

Uses `asio::steady_timer` + `async_wait` for the timeout
(unified with the existing signal handling under one
io_context). The `process_signals` lambda was restored as it
looks nicer.

Combines what were 4 incremental commits into one (the
separate window refactor from earlier remains). Updated
cuber.cpp and includes.
2026-05-05 23:43:33 +02:00
portersky 22d2bb1c40 refactor: separate window from opengl::context
Extract GLFW window management into a dedicated cbt::window
class (new files in cbt/). The opengl::context now only
handles GLAD setup and context activation (no more window
creation or GLFW init/terminate).

Updated main loop in cuber.cpp, CMakeLists.txt (to build
the new source), and AGENTS.md (docs + source layout).

Addresses the design note in context.cpp about mixing
concerns.
2026-05-05 23:37:19 +02:00
portersky 7a81b30d32 feat: enable Windows dark mode for GLFW window
- Add DwmSetWindowAttribute call for immersive dark mode
- Include glfw3native.h after glfw3.h on Windows
2026-05-05 22:52:24 +02:00
portersky 40ae94788e style: fix include ordering across all source files
- C++ std headers first, then third-party, then local
- Apply consistently in cbt/ and scenes/
2026-05-05 22:40:51 +02:00
20 changed files with 665 additions and 196 deletions
+3
View File
@@ -43,3 +43,6 @@ imgui.ini
# Compiled shaders # Compiled shaders
*.spv *.spv
# Screenshots
*.png
+4 -2
View File
@@ -34,7 +34,7 @@ To add a new dependency:
The project is split into static libraries: The project is split into static libraries:
- **`cbt_opengl`** — OpenGL abstraction (context, buffer, texture, vao, shader, descriptor) - **`cbt_opengl`** — OpenGL abstraction and window management (window, context, buffer, texture, vao, shader, descriptor)
- **`cbt_scene`** — Base scene class - **`cbt_scene`** — Base scene class
- **`scenes_cube`** — Cube scene implementation - **`scenes_cube`** — Cube scene implementation
@@ -102,8 +102,10 @@ cuber/
cbt/ # Project namespace (utilities) cbt/ # Project namespace (utilities)
scene.hpp # Base scene class scene.hpp # Base scene class
scene.cpp # Scene implementation scene.cpp # Scene implementation
window.hpp # GLFW window RAII wrapper
window.cpp # Window implementation
opengl/ # OpenGL abstraction layer opengl/ # OpenGL abstraction layer
context.hpp # OpenGL context RAII wrapper context.hpp # OpenGL context RAII wrapper (GLAD setup)
context.cpp # Context implementation context.cpp # Context implementation
buffer.hpp # Buffer resource (VBO, EBO, UBO, SSBO) buffer.hpp # Buffer resource (VBO, EBO, UBO, SSBO)
buffer.cpp # Buffer implementation buffer.cpp # Buffer implementation
+14 -2
View File
@@ -17,9 +17,11 @@ find_package(glfw3 REQUIRED)
find_package(glad REQUIRED) find_package(glad REQUIRED)
find_package(asio REQUIRED) find_package(asio REQUIRED)
find_package(glm REQUIRED) find_package(glm REQUIRED)
find_package(stb REQUIRED)
# OpenGL abstraction library # OpenGL abstraction library
add_library(cbt_opengl STATIC add_library(cbt_opengl STATIC
"cbt/window.cpp"
"cbt/opengl/context.cpp" "cbt/opengl/context.cpp"
"cbt/opengl/buffer.cpp" "cbt/opengl/buffer.cpp"
"cbt/opengl/texture.cpp" "cbt/opengl/texture.cpp"
@@ -31,7 +33,7 @@ target_include_directories(cbt_opengl PRIVATE ".")
target_compile_features(cbt_opengl PRIVATE cxx_std_23) target_compile_features(cbt_opengl PRIVATE cxx_std_23)
target_compile_options(cbt_opengl PRIVATE ${BASE_OPTIONS}) target_compile_options(cbt_opengl PRIVATE ${BASE_OPTIONS})
target_compile_definitions(cbt_opengl PRIVATE ${BASE_DEFINITIONS}) target_compile_definitions(cbt_opengl PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(cbt_opengl PUBLIC fmt::fmt glfw::glfw glad::glad) target_link_libraries(cbt_opengl PUBLIC fmt::fmt glfw::glfw glad::glad stb::stb)
# Scene base library # Scene base library
add_library(cbt_scene STATIC add_library(cbt_scene STATIC
@@ -53,10 +55,20 @@ target_compile_options(scenes_cube PRIVATE ${BASE_OPTIONS})
target_compile_definitions(scenes_cube PRIVATE ${BASE_DEFINITIONS}) target_compile_definitions(scenes_cube PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(scenes_cube PUBLIC cbt_scene glm::glm) target_link_libraries(scenes_cube PUBLIC cbt_scene glm::glm)
# Sphere scene
add_library(scenes_sphere STATIC
"scenes/sphere.cpp"
)
target_include_directories(scenes_sphere PRIVATE ".")
target_compile_features(scenes_sphere PRIVATE cxx_std_23)
target_compile_options(scenes_sphere PRIVATE ${BASE_OPTIONS})
target_compile_definitions(scenes_sphere PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(scenes_sphere PUBLIC cbt_scene glm::glm)
# Main executable # Main executable
add_executable(cuber "cuber.cpp") add_executable(cuber "cuber.cpp")
target_include_directories(cuber PRIVATE ".") target_include_directories(cuber PRIVATE ".")
target_compile_features(cuber PRIVATE cxx_std_23) target_compile_features(cuber PRIVATE cxx_std_23)
target_compile_options(cuber PRIVATE ${BASE_OPTIONS}) target_compile_options(cuber PRIVATE ${BASE_OPTIONS})
target_compile_definitions(cuber PRIVATE ${BASE_DEFINITIONS}) target_compile_definitions(cuber PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(cuber PRIVATE cbt_scene scenes_cube asio::asio ${BASE_LIBRARIES}) target_link_libraries(cuber PRIVATE cbt_scene scenes_cube scenes_sphere asio::asio ${BASE_LIBRARIES})
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/buffer.hpp"
#include "glad/glad.h" #include "glad/glad.h"
#include "cbt/opengl/buffer.hpp"
namespace cbt::opengl { namespace cbt::opengl {
buffer::buffer() { buffer::buffer() {
+15 -60
View File
@@ -1,27 +1,15 @@
#include <string_view>
#define GLFW_INCLUDE_NONE #define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h" #include "GLFW/glfw3.h"
#include "fmt/std.h"
#include "glad/glad.h"
#include "cbt/opengl/context.hpp" #include "cbt/opengl/context.hpp"
#include <string_view>
#include "glad/glad.h"
#include "fmt/std.h"
namespace cbt::opengl { namespace cbt::opengl {
auto context::init() -> bool {
if (!glfwInit()) {
fmt::print("Failed to initialize GLFW\n");
return false;
}
return true;
}
auto context::terminate() -> void {
glfwTerminate();
}
auto context::setup_gl() -> bool { auto context::setup_gl() -> bool {
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
fmt::print("Failed to initialize GLAD\n"); fmt::print("Failed to initialize GLAD\n");
@@ -40,58 +28,25 @@ auto context::print_info() -> void {
fmt::print("\n"); fmt::print("\n");
} }
context::context(std::string title, int width, int height) { context::context(window const& win) {
if (!init()) { glfwMakeContextCurrent(win.raw());
return;
}
m_initialized = true;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
m_window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!m_window) {
fmt::print("Failed to create window\n");
terminate();
return;
}
glfwMakeContextCurrent(m_window);
if (!setup_gl()) { if (!setup_gl()) {
terminate();
return; return;
} }
m_valid = true;
// Set initial viewport to match window size
glViewport(0, 0, win.width(), win.height());
} }
context::~context() { context::~context() {}
if (m_window) {
glfwDestroyWindow(m_window);
}
if (m_initialized) {
terminate();
}
}
auto context::should_close() const -> bool {
return m_window && glfwWindowShouldClose(m_window);
}
auto context::valid() const -> bool { auto context::valid() const -> bool {
return m_window != nullptr; return m_valid;
} }
auto context::swap_buffers() -> void { auto context::set_size(int width, int height) -> void {
glfwSwapBuffers(m_window); glViewport(0, 0, width, height);
}
auto context::poll_events() -> void {
glfwPollEvents();
}
auto context::raw() const -> GLFWwindow* {
return m_window;
} }
} }
+4 -13
View File
@@ -1,29 +1,20 @@
#pragma once #pragma once
#include <string> #include "cbt/window.hpp"
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
namespace cbt::opengl { namespace cbt::opengl {
class context { class context {
public: public:
explicit context(std::string title, int width, int height); explicit context(window const& win);
~context(); ~context();
auto should_close() const -> bool;
auto valid() const -> bool; auto valid() const -> bool;
auto swap_buffers() -> void; auto set_size(int width, int height) -> void;
auto poll_events() -> void;
auto raw() const -> GLFWwindow*;
private: private:
GLFWwindow* m_window = nullptr; bool m_valid = false;
bool m_initialized = false;
static auto init() -> bool;
static auto terminate() -> void;
static auto setup_gl() -> bool; static auto setup_gl() -> bool;
static auto print_info() -> void; static auto print_info() -> void;
}; };
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/descriptor.hpp"
#include "glad/glad.h" #include "glad/glad.h"
#include "cbt/opengl/descriptor.hpp"
namespace cbt::opengl { namespace cbt::opengl {
descriptor_set::descriptor_set() {} descriptor_set::descriptor_set() {}
+3 -3
View File
@@ -1,12 +1,12 @@
#pragma once #pragma once
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/texture.hpp"
#include <array> #include <array>
#include <optional> #include <optional>
#include <vector> #include <vector>
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/texture.hpp"
namespace cbt::opengl { namespace cbt::opengl {
struct descriptor_binding { struct descriptor_binding {
+2 -2
View File
@@ -1,10 +1,10 @@
#include "cbt/opengl/shader.hpp"
#include <iostream> #include <iostream>
#include <string> #include <string>
#include "fmt/std.h" #include "fmt/std.h"
#include "cbt/opengl/shader.hpp"
namespace cbt::opengl { namespace cbt::opengl {
shader::shader() {} shader::shader() {}
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/texture.hpp"
#include "glad/glad.h" #include "glad/glad.h"
#include "cbt/opengl/texture.hpp"
namespace cbt::opengl { namespace cbt::opengl {
texture::texture() { texture::texture() {
+1 -1
View File
@@ -8,6 +8,6 @@ auto scene::init() -> bool {
auto scene::update(float) -> void {} auto scene::update(float) -> void {}
auto scene::render() -> void {} auto scene::render(int, int) -> void {}
} }
+1 -1
View File
@@ -10,7 +10,7 @@ public:
virtual auto init() -> bool; virtual auto init() -> bool;
virtual auto update(float delta_time) -> void; virtual auto update(float delta_time) -> void;
virtual auto render() -> void; virtual auto render(int width, int height) -> void;
}; };
} }
+159
View File
@@ -0,0 +1,159 @@
#ifdef _WIN32
#include <dwmapi.h>
#include <windows.h>
#endif
#include <vector>
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
#ifdef _WIN32
#define GLFW_EXPOSE_NATIVE_WIN32
#include "GLFW/glfw3native.h"
#endif
#include "fmt/std.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "glad/glad.h"
#include "cbt/window.hpp"
namespace cbt {
auto window::init_glfw() -> bool {
if (!glfwInit()) {
fmt::print("Failed to initialize GLFW\n");
return false;
}
return true;
}
auto window::terminate_glfw() -> void {
glfwTerminate();
}
window::window(std::string title, int width, int height) {
if (!init_glfw()) {
return;
}
m_glfw_initialized = true;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
m_window = glfwCreateWindow(width, height, title.c_str(), nullptr, nullptr);
if (!m_window) {
fmt::print("Failed to create window\n");
terminate_glfw();
m_glfw_initialized = false;
return;
}
m_width = width;
m_height = height;
glfwSetWindowUserPointer(m_window, this);
glfwSetWindowSizeCallback(m_window, resize_callback_impl);
#ifdef _WIN32
auto hwnd = glfwGetWin32Window(m_window);
BOOL use_dark_mode = TRUE;
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &use_dark_mode, sizeof(use_dark_mode));
#endif
}
window::~window() {
if (m_window) {
glfwDestroyWindow(m_window);
}
if (m_glfw_initialized) {
terminate_glfw();
}
}
auto window::should_close() const -> bool {
return m_window && glfwWindowShouldClose(m_window);
}
auto window::valid() const -> bool {
return m_window != nullptr;
}
auto window::swap_buffers() -> void {
glfwSwapBuffers(m_window);
}
auto window::poll_events() -> void {
glfwPollEvents();
}
auto window::raw() const -> GLFWwindow* {
return m_window;
}
auto window::stop() -> void {
if (m_window) {
glfwSetWindowShouldClose(m_window, GLFW_TRUE);
}
}
auto window::width() const -> int {
return m_width;
}
auto window::height() const -> int {
return m_height;
}
auto window::on_resize(resize_callback cb) -> void {
m_resize_cb = cb;
}
auto window::resize_callback_impl(GLFWwindow* glfw_window, int width, int height) -> void {
auto* self = static_cast<window*>(glfwGetWindowUserPointer(glfw_window));
if (self) {
self->m_width = width;
self->m_height = height;
if (self->m_resize_cb) {
self->m_resize_cb(width, height);
}
}
}
auto window::screenshot(std::string filepath) const -> bool {
if (!m_window || !valid()) {
return false;
}
int width = 0;
int height = 0;
glfwGetFramebufferSize(m_window, &width, &height);
if (width <= 0 || height <= 0) {
return false;
}
std::vector<unsigned char> pixels(static_cast<size_t>(width * height * 4));
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
// Flip vertically (OpenGL reads bottom-up; PNG is top-down)
for (int y = 0; y < height / 2; ++y) {
int y2 = height - 1 - y;
for (int x = 0; x < width * 4; ++x) {
std::swap(pixels[static_cast<size_t>(y * width * 4 + x)],
pixels[static_cast<size_t>(y2 * width * 4 + x)]);
}
}
if (stbi_write_png(filepath.c_str(), width, height, 4, pixels.data(), width * 4) != 0) {
fmt::print("Screenshot saved to {}\n", filepath);
return true;
}
fmt::print("Failed to write screenshot to {}\n", filepath);
return false;
}
}
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <functional>
#include <string>
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
namespace cbt {
class window {
public:
explicit window(std::string title, int width, int height);
~window();
auto should_close() const -> bool;
auto valid() const -> bool;
auto swap_buffers() -> void;
auto poll_events() -> void;
auto raw() const -> GLFWwindow*;
auto stop() -> void;
auto screenshot(std::string filepath = "screenshot.png") const -> bool;
auto width() const -> int;
auto height() const -> int;
using resize_callback = std::function<void(int width, int height)>;
auto on_resize(resize_callback cb) -> void;
private:
GLFWwindow* m_window = nullptr;
int m_width = 0;
int m_height = 0;
resize_callback m_resize_cb;
bool m_glfw_initialized = false;
static auto init_glfw() -> bool;
static auto terminate_glfw() -> void;
static auto resize_callback_impl(GLFWwindow* glfw_window, int width, int height) -> void;
};
}
+98 -19
View File
@@ -1,35 +1,100 @@
#define GLFW_INCLUDE_NONE #define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
#include "cbt/opengl/context.hpp"
#include "scenes/cube.hpp"
#include <csignal> #include <csignal>
#include <chrono>
#include <string>
#include <string_view>
#include <asio.hpp> #include "GLFW/glfw3.h"
#include "asio.hpp"
#include "asio/steady_timer.hpp"
#include "fmt/std.h"
auto main(int, char const*[]) -> int { #include "cbt/window.hpp"
auto ctx = cbt::opengl::context("cuber", 1280, 720); #include "cbt/opengl/context.hpp"
#include "scenes/cube.hpp"
#include "scenes/sphere.hpp"
auto main(int argc, char const* argv[]) -> int {
float max_duration_seconds = 0.0f;
std::string_view scene_name = "cube";
bool take_screenshot = false;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i];
if (arg == "--help" || arg == "-h") {
fmt::print("Usage: {} [options]\n", argv[0]);
fmt::print(" --duration <seconds> Auto-terminate after N seconds (for testing/CI)\n");
fmt::print(" --scene <cube|sphere> Select initial scene (default: cube)\n");
fmt::print(" --screenshot Render one frame, save screenshot, and exit\n");
fmt::print(" S key Take screenshot (saved as screenshot.png)\n");
fmt::print(" 1/2 key Switch between cube/sphere scene\n");
return 0;
}
if (arg == "--duration" && i + 1 < argc) {
max_duration_seconds = std::stof(std::string(argv[++i]));
continue;
}
if (arg == "--scene" && i + 1 < argc) {
scene_name = argv[++i];
continue;
}
if (arg == "--screenshot") {
take_screenshot = true;
continue;
}
}
auto win = cbt::window("cuber", 1280, 720);
if (!win.valid()) {
return 1;
}
auto ctx = cbt::opengl::context(win);
if (!ctx.valid()) { if (!ctx.valid()) {
return 1; return 1;
} }
auto scn = cbt::scenes::cube(); // Wire up resize callback
if (!scn.init()) { win.on_resize([&ctx](int width, int height) {
ctx.set_size(width, height);
});
auto cube_scn = cbt::scenes::cube();
auto sphere_scn = cbt::scenes::sphere();
cbt::scene* active_scene = nullptr;
if (!cube_scn.init() || !sphere_scn.init()) {
return 1; return 1;
} }
if (scene_name == "sphere" || scene_name == "2") {
active_scene = &sphere_scn;
} else {
active_scene = &cube_scn;
}
// signal handling // signal handling + optional duration timer (via ASIO)
asio::io_context io; asio::io_context io;
asio::signal_set signals(io, SIGINT, SIGTERM); asio::signal_set signals(io, SIGINT, SIGTERM);
bool quit = false;
signals.async_wait([&](auto, auto) { signals.async_wait([&](auto, auto) {
quit = true; win.stop();
io.stop(); io.stop();
}); });
asio::steady_timer duration_timer(io);
if (max_duration_seconds > 0.0f) {
duration_timer.expires_after(std::chrono::milliseconds(
static_cast<long long>(max_duration_seconds * 1000.0f)));
duration_timer.async_wait([&win](auto ec) {
if (!ec) {
win.stop();
}
});
}
auto process_signals = [&]() -> void { auto process_signals = [&]() -> void {
while (io.poll()) {} while (io.poll()) {}
}; };
@@ -37,22 +102,36 @@ auto main(int, char const*[]) -> int {
// render loop // render loop
auto prev = std::chrono::steady_clock::now(); auto prev = std::chrono::steady_clock::now();
while (!ctx.should_close()) { while (!win.should_close()) {
process_signals(); process_signals();
if (quit || glfwGetKey(ctx.raw(), GLFW_KEY_Q) == GLFW_PRESS) { if (glfwGetKey(win.raw(), GLFW_KEY_Q) == GLFW_PRESS) {
break; win.stop();
}
if (glfwGetKey(win.raw(), GLFW_KEY_S) == GLFW_PRESS) {
win.screenshot();
}
if (glfwGetKey(win.raw(), GLFW_KEY_1) == GLFW_PRESS) {
active_scene = &cube_scn;
}
if (glfwGetKey(win.raw(), GLFW_KEY_2) == GLFW_PRESS) {
active_scene = &sphere_scn;
} }
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
auto dt = std::chrono::duration<float>(now - prev).count(); auto dt = std::chrono::duration<float>(now - prev).count();
prev = now; prev = now;
scn.update(dt); active_scene->update(dt);
scn.render(); active_scene->render(win.width(), win.height());
ctx.swap_buffers(); win.swap_buffers();
ctx.poll_events(); win.poll_events();
if (take_screenshot) {
win.screenshot();
win.stop();
}
} }
return 0; return 0;
+17 -43
View File
@@ -1,28 +1,17 @@
# ============================================================================== # ==============================================================================
# Find stb # Find stb
# ============================================================================== # ==============================================================================
# This module fetches the stb single-file public domain libraries. # This module fetches the stb single-file public domain libraries
# (stb_image_write.h for PNG writing, etc.).
# #
# Targets provided: # Targets provided:
# stb::stb - The stb library target # stb::stb - Interface library (add #define STB_IMAGE_WRITE_IMPLEMENTATION
# in exactly one .cpp before including "stb_image_write.h")
# #
# Variables set: # Variables set:
# stb_FOUND - TRUE if stb is available # stb_FOUND - TRUE if stb is available
# stb_LIBRARIES - The stb library target (stb::stb)
# stb_INCLUDE_DIR - Include directories for stb # stb_INCLUDE_DIR - Include directories for stb
# stb_VERSION - Version of stb (commit-based) # stb_VERSION - Version of stb (commit)
#
# Usage notes:
# stb headers are header-only but require an implementation macro to be
# defined in exactly ONE translation unit (.cpp file) before including the
# header, e.g.:
#
# #define STB_IMAGE_IMPLEMENTATION
# #include <stb_image.h>
#
# Each stb header has its own implementation macro. Do NOT define the macro
# in header files or in more than one translation unit or you will get
# duplicate symbol linker errors.
# ============================================================================== # ==============================================================================
if (DEFINED _FINDSTB_INCLUDED) if (DEFINED _FINDSTB_INCLUDED)
@@ -30,46 +19,31 @@ if (DEFINED _FINDSTB_INCLUDED)
endif() endif()
set(_FINDSTB_INCLUDED TRUE) set(_FINDSTB_INCLUDED TRUE)
# Use the version passed to find_package(), or default to 2.30 # Pin to a recent stable commit
if (DEFINED stb_FIND_VERSION AND NOT stb_FIND_VERSION STREQUAL "") set(STB_VERSION "master")
set(STB_VERSION "${stb_FIND_VERSION}")
else()
set(STB_VERSION "2fb8c5a3deb2110c89669f8d6f36e5833b556b44")
endif()
message(STATUS "Fetching stb ${STB_VERSION}") message(STATUS "Fetching stb ${STB_VERSION}")
include(FetchContent) include(FetchContent)
find_program(GIT_EXECUTABLE git) FetchContent_Declare(
if (GIT_EXECUTABLE)
set(STB_FETCH_METHOD "GIT")
else()
message(FATAL_ERROR "Fetch with zip not supported.")
endif()
if (STB_FETCH_METHOD STREQUAL "GIT")
FetchContent_Declare(
stb stb
GIT_REPOSITORY https://github.com/nothings/stb.git GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG ${STB_VERSION} GIT_TAG ${STB_VERSION}
) )
endif()
FetchContent_MakeAvailable(stb) FetchContent_MakeAvailable(stb)
if (NOT TARGET stb) add_library(stb INTERFACE)
add_library(stb INTERFACE) target_include_directories(stb INTERFACE "${stb_SOURCE_DIR}")
target_include_directories(stb SYSTEM INTERFACE "${stb_SOURCE_DIR}")
endif()
if (NOT TARGET stb::stb) add_library(stb::stb ALIAS stb)
add_library(stb::stb ALIAS stb)
endif()
set(stb_FOUND TRUE) set(stb_FOUND TRUE)
set(stb_LIBRARIES stb::stb)
set(stb_VERSION "${STB_VERSION}")
set(stb_INCLUDE_DIR "${stb_SOURCE_DIR}") set(stb_INCLUDE_DIR "${stb_SOURCE_DIR}")
set(stb_VERSION "${STB_VERSION}")
set(STB_LICENSE_FILE "${stb_SOURCE_DIR}/LICENSE" CACHE FILEPATH "Path to stb license file") # Mark as SYSTEM includes
set_target_properties(stb PROPERTIES
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${stb_SOURCE_DIR}"
)
+6 -7
View File
@@ -1,11 +1,10 @@
#include "scenes/cube.hpp"
#include <array> #include <array>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "glad/glad.h" #include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "scenes/cube.hpp"
namespace cbt::scenes { namespace cbt::scenes {
@@ -24,14 +23,14 @@ auto cube::init() -> bool {
auto cube::update(float) -> void {} auto cube::update(float) -> void {}
auto cube::render() -> void { auto cube::render(int width, int height) -> void {
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration<float>(now - m_start).count(); auto elapsed = std::chrono::duration<float>(now - m_start).count();
glClearColor(0.15f, 0.15f, 0.2f, 1.0f); glClearColor(0.15f, 0.15f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auto aspect = 1280.0f / 720.0f; auto aspect = float(width) / float(height);
auto proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f); auto proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
auto view = glm::translate(glm::mat4{1.0f}, glm::vec3{0.0f, 0.0f, -3.0f}); auto view = glm::translate(glm::mat4{1.0f}, glm::vec3{0.0f, 0.0f, -3.0f});
auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.5f, 0.3f}); auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.5f, 0.3f});
+6 -5
View File
@@ -1,13 +1,14 @@
#pragma once #pragma once
#include <chrono>
#include "glm/glm.hpp"
#include "cbt/scene.hpp" #include "cbt/scene.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/buffer.hpp" #include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/vao.hpp" #include "cbt/opengl/vao.hpp"
#include <glm/glm.hpp>
namespace cbt::scenes { namespace cbt::scenes {
class cube final : public scene { class cube final : public scene {
@@ -15,7 +16,7 @@ public:
cube(); cube();
auto init() -> bool override; auto init() -> bool override;
auto update(float delta_time) -> void override; auto update(float delta_time) -> void override;
auto render() -> void override; auto render(int width, int height) -> void override;
private: private:
opengl::shader m_prog; opengl::shader m_prog;
+214
View File
@@ -0,0 +1,214 @@
#include <cstddef>
#include <cmath>
#include <vector>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "scenes/sphere.hpp"
namespace cbt::scenes {
sphere::sphere() {
m_start = std::chrono::steady_clock::now();
}
auto sphere::init() -> bool {
if (!build_shader()) {
return false;
}
build_mesh();
glEnable(GL_DEPTH_TEST);
return true;
}
auto sphere::update(float) -> void {}
auto sphere::render(int width, int height) -> void {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration<float>(now - m_start).count();
glClearColor(0.15f, 0.15f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auto aspect = float(width) / float(height);
auto proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
auto view = glm::translate(glm::mat4{1.0f}, glm::vec3{0.0f, 0.0f, -4.0f});
auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.3f, 0.2f});
m_prog.use();
glUniformMatrix4fv(m_loc_proj, 1, GL_FALSE, glm::value_ptr(proj));
glUniformMatrix4fv(m_loc_view, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(m_loc_model, 1, GL_FALSE, glm::value_ptr(model));
m_vao.bind();
glDrawElements(GL_TRIANGLES, m_index_count, GL_UNSIGNED_INT, nullptr);
m_vao.unbind();
m_prog.unuse();
}
auto sphere::build_mesh() -> void {
struct vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 uv;
glm::vec3 color;
};
std::uint32_t const div = 32;
std::vector<vertex> vertices;
std::vector<std::uint32_t> indices;
// Distinct colors for each cube face so seams are visible
glm::vec3 const face_colors[6] = {
{1.0f, 0.2f, 0.2f}, // +X red
{0.2f, 1.0f, 0.2f}, // -X green
{0.2f, 0.2f, 1.0f}, // +Y blue
{1.0f, 1.0f, 0.2f}, // -Y yellow
{1.0f, 0.2f, 1.0f}, // +Z magenta
{0.2f, 1.0f, 1.0f}, // -Z cyan
};
// Generate 6 cube faces, each with div x div vertices
auto add_face = [&](glm::vec3 const& center, glm::vec3 const& u_axis,
glm::vec3 const& v_axis, std::uint32_t face_idx) -> void {
for (std::uint32_t i = 0; i < div; ++i) {
for (std::uint32_t j = 0; j < div; ++j) {
float const s = float(i) / float(div - 1) * 2.0f - 1.0f;
float const t = float(j) / float(div - 1) * 2.0f - 1.0f;
// Position on cube face
glm::vec3 pos = center + u_axis * s + v_axis * t;
// FIX: normalize to project onto unit sphere
// (the original nrz.cpp used a broken formula with p=50.0)
float const len = glm::length(pos);
glm::vec3 normal = pos / len;
vertices.push_back({normal, normal, {float(i) / float(div - 1), float(j) / float(div - 1)}, face_colors[face_idx]});
}
}
};
// +X face (right)
add_face(glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{0.0f, 0.0f, 1.0f}, 0);
// -X face (left)
add_face(glm::vec3{-1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{0.0f, 0.0f, -1.0f}, 1);
// +Y face (top)
add_face(glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 0.0f, -1.0f}, 2);
// -Y face (bottom)
add_face(glm::vec3{0.0f, -1.0f, 0.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 0.0f, 1.0f}, 3);
// +Z face (front)
add_face(glm::vec3{0.0f, 0.0f, 1.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, 4);
// -Z face (back)
add_face(glm::vec3{0.0f, 0.0f, -1.0f}, glm::vec3{-1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, 5);
// Generate indices for each face
std::uint32_t offset = 0;
for (std::uint32_t face = 0; face < 6; ++face) {
for (std::uint32_t i = 0; i < div - 1; ++i) {
for (std::uint32_t j = 0; j < div - 1; ++j) {
std::uint32_t const a = offset + i * div + j;
std::uint32_t const b = offset + (i + 1) * div + j;
std::uint32_t const c = offset + (i + 1) * div + j + 1;
std::uint32_t const d = offset + i * div + j + 1;
// Two triangles per quad (consistent winding)
indices.push_back(a);
indices.push_back(b);
indices.push_back(d);
indices.push_back(b);
indices.push_back(c);
indices.push_back(d);
}
}
offset += div * div;
}
m_index_count = static_cast<GLsizei>(indices.size());
m_vbo.upload(vertices.data(), vertices.size() * sizeof(vertex));
m_ebo.upload(indices.data(), indices.size() * sizeof(std::uint32_t));
m_vao.bind();
m_vbo.bind();
m_ebo.bind();
// location 0: position (vec3)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), nullptr);
// location 1: normal (vec3)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(3 * sizeof(float)));
// location 2: uv (vec2)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(6 * sizeof(float)));
// location 3: color (vec3)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(8 * sizeof(float)));
m_vao.unbind();
m_vbo.unbind();
m_ebo.unbind();
}
auto sphere::build_shader() -> bool {
char const* vert_src = R"glsl(
#version 410 core
layout(location = 0) in vec3 a_pos;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_uv;
layout(location = 3) in vec3 a_color;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_proj;
out vec3 v_normal;
out vec3 v_world_pos;
out vec2 v_uv;
out vec3 v_color;
void main() {
gl_Position = u_proj * u_view * u_model * vec4(a_pos, 1.0);
v_normal = mat3(u_model) * a_normal;
v_world_pos = (u_model * vec4(a_pos, 1.0)).xyz;
v_uv = a_uv;
v_color = a_color;
}
)glsl";
char const* frag_src = R"glsl(
#version 410 core
in vec3 v_normal;
in vec3 v_world_pos;
in vec2 v_uv;
in vec3 v_color;
out vec4 frag_color;
void main() {
vec3 light_dir = normalize(vec3(1.0, 1.0, 2.0));
vec3 normal = normalize(v_normal);
float diff = max(dot(normal, light_dir), 0.0);
vec3 ambient = vec3(0.2);
vec3 result = (ambient + diff * 0.8) * v_color;
frag_color = vec4(result, 1.0);
}
)glsl";
if (!m_prog.compile_vertex(vert_src) || !m_prog.compile_fragment(frag_src) || !m_prog.link()) {
return false;
}
m_loc_proj = glGetUniformLocation(m_prog.id(), "u_proj");
m_loc_view = glGetUniformLocation(m_prog.id(), "u_view");
m_loc_model = glGetUniformLocation(m_prog.id(), "u_model");
return true;
}
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <chrono>
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/vao.hpp"
namespace cbt::scenes {
class sphere final : public scene {
public:
sphere();
auto init() -> bool override;
auto update(float delta_time) -> void override;
auto render(int width, int height) -> void override;
private:
opengl::shader m_prog;
opengl::buffer m_vbo;
opengl::buffer m_ebo{opengl::buffer_type::index};
opengl::vao m_vao;
GLint m_loc_proj = -1;
GLint m_loc_view = -1;
GLint m_loc_model = -1;
GLsizei m_index_count = 0;
std::chrono::steady_clock::time_point m_start;
auto build_mesh() -> void;
auto build_shader() -> bool;
};
}