Compare commits

...

16 Commits

Author SHA1 Message Date
portersky 6bfde6c6fb feat: add Cornell Box scene with orbit camera
- gfx::pipeline gains bind_vec3/bind_float for passing arbitrary
  uniforms to shaders (used for light position/color)
- scene base gains on_mouse_drag virtual hook; cuber.cpp polls
  left-mouse delta each frame and forwards it to the active scene
- cornell_box scene: 5-wall room (red/green/white), two pre-rotated
  white boxes with proportions from the original paper, an unlit
  ceiling light panel, and a point-light Lambert shader
- left-click drag orbits the camera around the scene origin;
  pitch is clamped to ±80° to prevent gimbal flip
- key 3 / --scene cornell_box selects the scene
2026-05-11 16:35:26 +02:00
portersky 5b4743ff8f docs: document gfx::pipeline + add Markdown style rule to AGENTS.md + README styling
- Added comprehensive "Pipeline Abstraction" section to README.md
  (for dummies, with step-by-step examples, render-to-texture, and
  post-processing).
- Added "Documentation (Markdown)" section to AGENTS.md with the
  max-80-cols rule (exceptions for tables/code blocks).
- Applied consistent Markdown styling to AGENTS.md and README.md
  (wrapped text at 80 cols, bold, code spans, cleaned duplicates).
- Moved scenes libraries to  (cleaned main
  CMakeLists.txt significantly).

No behavior change. All follows AGENTS.md conventions.
2026-05-06 00:55:53 +02:00
portersky 6c1096fbb8 refactor: move scenes to dedicated CMakeLists.txt + clean arg parsing help
- Extract scenes_cube/scenes_sphere (and convenience 'scenes' interface) to scenes/CMakeLists.txt.
- Main CMakeLists.txt now much cleaner (single add_subdirectory(scenes)).
- Updated help text in cuber.cpp to clearly separate Flags vs Keys (less confusing 'S' section).
- No behavior change; depth/viewport/state/post-processing fixes from earlier remain.

Followed project conventions and AGENTS.md commit rules.
2026-05-06 00:50:09 +02:00
portersky c83561c0fa docs: update README with screenshot and gfx pipeline notes
- Add preview.png of the new sphere (with render-to-texture + post-processing).
- Update scenes description and usage.
- Minor .gitignore tweaks.
2026-05-06 00:43:34 +02:00
portersky 5ec8cfc735 feat: add gfx pipeline abstraction with render targets
Add cbt/gfx layer (pipeline + render_target) inspired by Sokol but
C++-friendly with RAII and pipeline_desc. Supports building full
graphics pipelines (VS/IA/raster/PS/OM), render-to-texture (FBO),
and post-processing steps (scene -> RT -> fullscreen quad with
sampler + vignette).

- Depth/state/viewport/texture cleanup for reliable scene switching.
- Updated cube/sphere to demonstrate (sphere uses RT+post).
- Vulkan backend easy via PIMPL in impl (same public API).
- Fixed depth test, viewport restore, and state leakage on switch.

Followed coding conventions (snake_case, trailing returns, east const,
include order, no ; after ns/class, etc.).
2026-05-06 00:43:00 +02:00
portersky 98673b57ff docs: update README and AGENTS.md to match current project
Updated project description from 'cube timer' to 'OpenGL 3D
renderer with multiple scenes'. Added usage section with CLI
flags and key bindings. Listed both available scenes.

Updated AGENTS.md to include the sphere scene in static
libraries and source layout. Normalized run command to
./build/cuber for cross-platform consistency.
2026-05-06 00:15:43 +02:00
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
26 changed files with 1744 additions and 321 deletions
+46 -19
View File
@@ -2,7 +2,7 @@
## Project Overview
`cuber` is a simple cube timer application.
`cuber` is an OpenGL 3D renderer with multiple scenes.
## Build System
@@ -12,11 +12,11 @@
### Commands
```sh
cmake -S . -B build -GNinja
ninja -C build
.\build\cuber.exe
```
```sh
cmake -S . -B build -GNinja
ninja -C build
./build/cuber
```
### Dependencies
@@ -34,9 +34,13 @@ To add a new dependency:
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
- **`scenes_cube`** — Cube scene implementation
- **`scenes_cube`** — Spinning cube scene implementation
- **`scenes_sphere`** — Cube-to-sphere mapped mesh with diffuse lighting
- **`scenes_cornell_box`** — Classic Cornell Box with Lambert shading and
a ceiling area light
### CMake Module Path
@@ -51,10 +55,13 @@ to the custom scripts instead of system-installed packages.
- **No semicolons after closing braces** for namespaces/classes
- `auto` for obvious types (e.g. `auto main(...) -> int`)
- **East const** (e.g. `char const*` not `const char*`)
- **Trailing return type** for all function definitions, including operators (e.g. `auto operator=(T&&) noexcept -> T&`)
- **Public members first** in class declarations, private members at the bottom
- **Trailing return type** for all function definitions, including
operators (e.g. `auto operator=(T&&) noexcept -> T&`)
- **Public members first** in class declarations, private members at the
bottom
- `<>` includes only for system headers (std, OS, etc.)
- `""` includes for third-party dependencies (e.g. `fmt`, `nlohmann/json`)
- `""` includes for third-party dependencies (e.g. `fmt`,
`nlohmann/json`)
- **Naming:** `snake_case` for variables, functions, and classes
- **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants
- Include order:
@@ -81,29 +88,45 @@ to the custom scripts instead of system-installed packages.
- Follow the 50/72 rule:
- Subject line: max 50 characters
- Body lines: wrapped at 72 characters
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`, etc.)
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`,
etc.)
- Separate subject from body with a blank line
- Do **not** add a `Co-Authored-By` trailer or any agent/AI attribution
Example:
```
feat: add stopwatch timer
```
feat: add stopwatch timer
Replace Hello World with a live stopwatch that prints elapsed time
in HH:MM:SS.mmm format, updating every 10ms with color output.
```
Replace Hello World with a live stopwatch that prints elapsed time
in HH:MM:SS.mmm format, updating every 10ms with color output.
```
## Documentation (Markdown)
- Wrap normal text and lists at **max 80 columns** (for readability in
terminals and editors).
- **Exceptions**: Tables and code blocks (```` ``` ````) can exceed 80
columns when formatting requires it (e.g. trees, alignment).
- Use standard Markdown: `**bold**`, `` `inline code` ``, `##` headings,
`-` or numbered lists, fenced code blocks with language hints
(```` ```cpp ````, ```` ```sh ````).
- Keep examples concise, up-to-date, and self-documenting.
- This file (`AGENTS.md`) follows its own rules.
## Source Layout
```
```text
cuber/
CMakeLists.txt # Build configuration
cuber.cpp # Entry point
cbt/ # Project namespace (utilities)
scene.hpp # Base scene class
scene.cpp # Scene implementation
window.hpp # GLFW window RAII wrapper
window.cpp # Window implementation
opengl/ # OpenGL abstraction layer
context.hpp # OpenGL context RAII wrapper
context.hpp # OpenGL context RAII wrapper (GLAD setup)
context.cpp # Context implementation
buffer.hpp # Buffer resource (VBO, EBO, UBO, SSBO)
buffer.cpp # Buffer implementation
@@ -118,6 +141,10 @@ cuber/
scenes/ # Application scenes
cube.hpp # Spinning cube scene
cube.cpp # Cube scene implementation
sphere.hpp # Cube-to-sphere mapped mesh
sphere.cpp # Sphere scene implementation
cornell_box.hpp # Cornell Box scene
cornell_box.cpp # Cornell Box scene implementation
deps/ # Custom Find*.cmake scripts
Findfmt.cmake # fmt library
```
+6 -11
View File
@@ -17,21 +17,24 @@ find_package(glfw3 REQUIRED)
find_package(glad REQUIRED)
find_package(asio REQUIRED)
find_package(glm REQUIRED)
find_package(stb REQUIRED)
# OpenGL abstraction library
add_library(cbt_opengl STATIC
"cbt/window.cpp"
"cbt/opengl/context.cpp"
"cbt/opengl/buffer.cpp"
"cbt/opengl/texture.cpp"
"cbt/opengl/descriptor.cpp"
"cbt/opengl/shader.cpp"
"cbt/opengl/vao.cpp"
"cbt/gfx.cpp"
)
target_include_directories(cbt_opengl PRIVATE ".")
target_compile_features(cbt_opengl PRIVATE cxx_std_23)
target_compile_options(cbt_opengl PRIVATE ${BASE_OPTIONS})
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 glm::glm)
# Scene base library
add_library(cbt_scene STATIC
@@ -43,15 +46,7 @@ target_compile_options(cbt_scene PRIVATE ${BASE_OPTIONS})
target_compile_definitions(cbt_scene PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(cbt_scene PUBLIC cbt_opengl)
# Application scenes
add_library(scenes_cube STATIC
"scenes/cube.cpp"
)
target_include_directories(scenes_cube PRIVATE ".")
target_compile_features(scenes_cube PRIVATE cxx_std_23)
target_compile_options(scenes_cube PRIVATE ${BASE_OPTIONS})
target_compile_definitions(scenes_cube PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(scenes_cube PUBLIC cbt_scene glm::glm)
add_subdirectory(scenes)
# Main executable
add_executable(cuber "cuber.cpp")
@@ -59,4 +54,4 @@ target_include_directories(cuber PRIVATE ".")
target_compile_features(cuber PRIVATE cxx_std_23)
target_compile_options(cuber PRIVATE ${BASE_OPTIONS})
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 asio::asio ${BASE_LIBRARIES})
+132 -3
View File
@@ -1,6 +1,8 @@
# cuber
`cuber` is a simple cube timer.
`cuber` is an OpenGL 3D renderer with multiple scenes.
![Screenshot of sphere](./res/preview.png)
## Requirements
@@ -8,6 +10,9 @@
- Ninja
- C++23 compiler
All dependencies (fmt, GLFW, GLAD, asio, GLM, stb) are fetched
automatically via CMake FetchContent.
## Development
**Configure**:
@@ -24,7 +29,131 @@ ninja -C build
**Run**:
```bash
./build/cuber.exe
```sh
./build/cuber
```
## Usage
```
--duration <seconds> Auto-terminate after N seconds (for testing/CI)
--scene <cube|sphere> Select initial scene (default: cube)
--screenshot Render one frame, save screenshot, and exit
S key Take screenshot (saved as screenshot.png)
1/2 key Switch between cube/sphere scene
Q key Quit
```
## Scenes
- **cube** — spinning colored cube with per-face colors
- **sphere** — cube-to-sphere mapped mesh with per-face colors and
diffuse lighting (uses the new pipeline with render-to-texture + post-processing)
## Pipeline Abstraction (`cbt::gfx`)
The project now has a clean, easy-to-use **graphics pipeline** layer in
`cbt/gfx.hpp`. Think of it like a simple "drawing recipe" that hides
all the messy OpenGL details (shaders, buffers, VAOs, uniforms,
framebuffers). It's inspired by libraries like Sokol but made for C++
with RAII classes, `pipeline_desc` structs, and beginner-friendly
methods.
### Why it exists (for dummies)
- **No more copy-paste GL calls** in your scenes.
- **Easy to add effects**: Render to a texture (offscreen), then do
post-processing (blur, vignette, color grading, bloom, SSAO, etc.).
- **Future-proof**: The same code works if we add a Vulkan backend later
(just swap the internal `impl`—no changes to your scene code).
- **Switching scenes works smoothly** (no more glitches from leftover
GL state like depth test or bound textures).
It abstracts the typical graphics pipeline stages you might see in
diagrams (vertex shader, rasterizer, pixel shader, output merger) into
one simple object. Post-processing is a second "step" after the main
draw.
### How to use it (step-by-step for dummies)
1. **Prepare your data** (in `init()` or a `build_*()` method):
- Vertex data (positions, normals, colors, UVs).
- Index data (optional, for triangles).
- Attribute description (where each piece of data lives in the
vertex, e.g. location 0 = position at offset 0).
- Shader source code (vertex + fragment as raw strings).
2. **Create a `pipeline_desc`** (the recipe):
```cpp
gfx::pipeline_desc desc {
.vertex_data = std::as_bytes(std::span{my_vertices}),
.index_data = std::as_bytes(std::span{my_indices}),
.attributes = {
{.location = 0, .num_components = 3, .offset = 0}, // pos
{.location = 1, .num_components = 3, .offset = 12}, // normal
// ... more
},
.vertex_stride = sizeof(MyVertex),
.vertex_shader_src = my_vert_src,
.fragment_shader_src = my_frag_src,
.depth_test = true, // on for 3D, off for 2D post-process
};
```
3. **Build the pipeline**:
```cpp
m_pipeline = gfx::pipeline{desc};
if (!m_pipeline.valid()) { /* error */ }
```
4. **Draw it** (in `render()`):
```cpp
m_pipeline.draw(model_matrix, view_matrix, proj_matrix);
```
### Render-to-Texture + Post-Processing (the cool part)
Use `gfx::render_target` for offscreen rendering (like a temporary
canvas):
```cpp
// In class
gfx::render_target m_rt{0, 0};
gfx::pipeline m_post_pipeline; // second pipeline for effects
// In init()
m_rt.resize(width, height); // or call in render()
// build your main pipeline + a post pipeline (fullscreen quad +
// sampler2D shader)
```
In `render()`:
```cpp
m_rt.resize(width, height);
m_rt.bind(); // draw to texture instead of screen
// ... clear, draw main scene pipeline ...
m_rt.unbind();
// Post-processing step
m_post_pipeline.bind_texture("u_texture", m_rt.color_id(), 0);
m_post_pipeline.draw(...); // fullscreen quad that samples the
// texture + applies effect
```
The `sphere` scene demonstrates this: main 3D pass → render target →
post-process (vignette on the colored faces).
### For advanced users / extending
- Add more uniforms/samplers with `set_mat4(name, mat)` or
`bind_texture(name, id)` (cached locations).
- To add Vulkan: implement a new `impl` in `gfx.cpp` that uses
`VkPipeline`, `VkFramebuffer`, etc. (the public API stays identical).
- See `scenes/sphere.cpp` for a full example (including fullscreen
quad for post-processing).
This keeps your scene code tiny and clean while giving you powerful
graphics features.
## Scenes
- **cube** — spinning colored cube with per-face colors
- **sphere** — cube-to-sphere mapped mesh with per-face colors and
diffuse lighting (uses the new pipeline with render-to-texture +
post-processing)
+347
View File
@@ -0,0 +1,347 @@
#include <cstddef>
#include <utility>
#include "cbt/gfx.hpp"
#include "glad/glad.h"
#include "glm/gtc/type_ptr.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/vao.hpp"
#include "cbt/opengl/texture.hpp"
namespace cbt::gfx {
struct pipeline::impl {
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;
GLsizei m_vertex_count = 0;
bool m_valid = false;
bool m_uses_index = false;
bool m_depth_test = true;
GLenum m_draw_mode = GL_TRIANGLES;
GLenum m_index_gl_type = GL_UNSIGNED_INT;
auto build(pipeline_desc const& desc) -> bool;
auto bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void;
auto bind_vec3(char const* name, glm::vec3 const& v) const -> void;
auto bind_float(char const* name, float v) const -> void;
};
auto pipeline::impl::build(pipeline_desc const& desc) -> bool {
if (!desc.vertex_shader_src || !desc.fragment_shader_src ||
desc.vertex_data.empty() || desc.attributes.empty() || desc.vertex_stride == 0) {
return false;
}
if (!m_prog.compile_vertex(desc.vertex_shader_src) ||
!m_prog.compile_fragment(desc.fragment_shader_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");
m_vbo.upload(desc.vertex_data.data(), desc.vertex_data.size());
m_vertex_count = static_cast<GLsizei>(desc.vertex_data.size() / desc.vertex_stride);
m_uses_index = !desc.index_data.empty();
if (m_uses_index) {
m_ebo.upload(desc.index_data.data(), desc.index_data.size());
std::size_t const idx_size = (desc.index_type_ == index_type::uint16)
? sizeof(std::uint16_t)
: sizeof(std::uint32_t);
m_index_count = static_cast<GLsizei>(desc.index_data.size() / idx_size);
m_index_gl_type = (desc.index_type_ == index_type::uint16)
? GL_UNSIGNED_SHORT
: GL_UNSIGNED_INT;
}
m_draw_mode = GL_TRIANGLES;
m_depth_test = desc.depth_test;
m_vao.bind();
m_vbo.bind();
if (m_uses_index) {
m_ebo.bind();
}
for (auto const& attr : desc.attributes) {
glEnableVertexAttribArray(attr.location);
glVertexAttribPointer(
attr.location,
static_cast<GLint>(attr.num_components),
GL_FLOAT,
GL_FALSE,
static_cast<GLsizei>(desc.vertex_stride),
reinterpret_cast<void*>(static_cast<std::uintptr_t>(attr.offset))
);
}
m_vao.unbind();
m_vbo.unbind();
if (m_uses_index) {
m_ebo.unbind();
}
if (desc.depth_test) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
m_valid = true;
return true;
}
auto pipeline::impl::bind_vec3(char const* name, glm::vec3 const& v) const -> void {
if (!m_prog.valid()) return;
m_prog.use();
GLint const loc = glGetUniformLocation(m_prog.id(), name);
if (loc != -1) {
glUniform3fv(loc, 1, glm::value_ptr(v));
}
m_prog.unuse();
}
auto pipeline::impl::bind_float(char const* name, float v) const -> void {
if (!m_prog.valid()) return;
m_prog.use();
GLint const loc = glGetUniformLocation(m_prog.id(), name);
if (loc != -1) {
glUniform1f(loc, v);
}
m_prog.unuse();
}
auto pipeline::impl::bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void {
if (!m_prog.valid()) {
return;
}
GLint loc = glGetUniformLocation(m_prog.id(), sampler_name);
if (loc != -1) {
glUniform1i(loc, static_cast<GLint>(unit));
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, texture_id);
}
}
pipeline::pipeline() : m_impl(std::make_unique<impl>()) {}
pipeline::pipeline(pipeline_desc const& desc) : m_impl(std::make_unique<impl>()) {
m_impl->build(desc);
}
pipeline::pipeline(pipeline&& other) noexcept
: m_impl(std::move(other.m_impl)) {}
auto pipeline::operator=(pipeline&& other) noexcept -> pipeline& {
if (this != &other) {
m_impl = std::move(other.m_impl);
}
return *this;
}
pipeline::~pipeline() = default;
auto pipeline::valid() const -> bool {
return m_impl && m_impl->m_valid;
}
auto pipeline::draw(
glm::mat4 const& model,
glm::mat4 const& view,
glm::mat4 const& proj
) const -> void {
if (!valid()) {
return;
}
auto& impl = *m_impl;
impl.m_prog.use();
if (impl.m_loc_proj != -1) {
glUniformMatrix4fv(impl.m_loc_proj, 1, GL_FALSE, glm::value_ptr(proj));
}
if (impl.m_loc_view != -1) {
glUniformMatrix4fv(impl.m_loc_view, 1, GL_FALSE, glm::value_ptr(view));
}
if (impl.m_loc_model != -1) {
glUniformMatrix4fv(impl.m_loc_model, 1, GL_FALSE, glm::value_ptr(model));
}
if (impl.m_depth_test) {
glEnable(GL_DEPTH_TEST);
} else {
glDisable(GL_DEPTH_TEST);
}
impl.m_vao.bind();
if (impl.m_uses_index) {
glDrawElements(impl.m_draw_mode, impl.m_index_count, impl.m_index_gl_type, nullptr);
} else {
glDrawArrays(impl.m_draw_mode, 0, impl.m_vertex_count);
}
impl.m_vao.unbind();
impl.m_prog.unuse();
// Reset texture state to prevent leakage on scene switches or between passes
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 0);
}
auto pipeline::bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void {
if (m_impl) {
m_impl->bind_texture(sampler_name, texture_id, unit);
}
}
auto pipeline::bind_vec3(char const* name, glm::vec3 const& v) const -> void {
if (m_impl) {
m_impl->bind_vec3(name, v);
}
}
auto pipeline::bind_float(char const* name, float v) const -> void {
if (m_impl) {
m_impl->bind_float(name, v);
}
}
struct render_target::impl {
GLuint m_fbo = 0;
opengl::texture m_color{opengl::texture_target::_2d};
GLuint m_depth_rbo = 0;
int m_width = 0;
int m_height = 0;
bool m_valid = false;
auto build(int width, int height) -> bool;
auto cleanup() -> void;
};
auto render_target::impl::build(int width, int height) -> bool {
m_width = width;
m_height = height;
glGenFramebuffers(1, &m_fbo);
if (m_fbo == 0) {
return false;
}
// Color texture
m_color = opengl::texture(opengl::texture_target::_2d);
m_color.bind(0);
m_color.upload(nullptr, width, height, opengl::texture_format::rgba, opengl::texture_type::ubyte);
m_color.set_filter(GL_LINEAR, GL_LINEAR);
m_color.set_wrap(GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
m_color.unbind();
// Depth renderbuffer
glGenRenderbuffers(1, &m_depth_rbo);
glBindRenderbuffer(GL_RENDERBUFFER, m_depth_rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// Attach to FBO
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_color.id(), 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_depth_rbo);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (status != GL_FRAMEBUFFER_COMPLETE) {
cleanup();
return false;
}
m_valid = true;
return true;
}
auto render_target::impl::cleanup() -> void {
if (m_fbo != 0) {
glDeleteFramebuffers(1, &m_fbo);
m_fbo = 0;
}
if (m_depth_rbo != 0) {
glDeleteRenderbuffers(1, &m_depth_rbo);
m_depth_rbo = 0;
}
m_valid = false;
}
render_target::render_target(int width, int height) : m_impl(std::make_unique<impl>()) {
m_impl->build(width, height);
}
render_target::render_target(render_target&& other) noexcept
: m_impl(std::move(other.m_impl)) {}
auto render_target::operator=(render_target&& other) noexcept -> render_target& {
if (this != &other) {
m_impl = std::move(other.m_impl);
}
return *this;
}
render_target::~render_target() {
if (m_impl) {
m_impl->cleanup();
}
}
auto render_target::bind() const -> void {
if (!valid()) return;
glBindFramebuffer(GL_FRAMEBUFFER, m_impl->m_fbo);
glViewport(0, 0, m_impl->m_width, m_impl->m_height);
}
auto render_target::unbind() const -> void {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
auto render_target::color_id() const -> std::uint32_t {
return m_impl ? m_impl->m_color.id() : 0;
}
auto render_target::width() const -> int {
return m_impl ? m_impl->m_width : 0;
}
auto render_target::height() const -> int {
return m_impl ? m_impl->m_height : 0;
}
auto render_target::valid() const -> bool {
return m_impl && m_impl->m_valid;
}
auto render_target::resize(int width, int height) -> void {
if (m_impl && m_impl->m_width == width && m_impl->m_height == height) {
return;
}
if (m_impl) {
m_impl->cleanup();
}
if (!m_impl) {
m_impl = std::make_unique<impl>();
}
m_impl->build(width, height);
}
} // namespace cbt::gfx
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <span>
#include <vector>
#include "glm/glm.hpp"
namespace cbt::gfx {
enum class primitive_type {
triangles
};
enum class index_type {
uint16,
uint32
};
struct attribute_desc {
std::uint32_t location = 0;
std::uint32_t num_components = 3;
std::uint32_t offset = 0;
};
struct pipeline_desc {
std::span<std::byte const> vertex_data{};
std::span<std::byte const> index_data{};
std::vector<attribute_desc> attributes{};
std::uint32_t vertex_stride = 0;
char const* vertex_shader_src = nullptr;
char const* fragment_shader_src = nullptr;
bool depth_test = true;
primitive_type primitive = primitive_type::triangles;
index_type index_type_ = index_type::uint32;
};
class pipeline {
public:
pipeline();
explicit pipeline(pipeline_desc const& desc);
pipeline(pipeline const&) = delete;
pipeline(pipeline&& other) noexcept;
auto operator=(pipeline const&) -> pipeline& = delete;
auto operator=(pipeline&& other) noexcept -> pipeline&;
~pipeline();
auto valid() const -> bool;
auto draw(glm::mat4 const& model, glm::mat4 const& view, glm::mat4 const& proj) const -> void;
auto bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit = 0) const -> void;
auto bind_vec3(char const* name, glm::vec3 const& v) const -> void;
auto bind_float(char const* name, float v) const -> void;
private:
struct impl;
std::unique_ptr<impl> m_impl;
};
class render_target {
public:
explicit render_target(int width, int height);
render_target(render_target const&) = delete;
render_target(render_target&& other) noexcept;
auto operator=(render_target const&) -> render_target& = delete;
auto operator=(render_target&& other) noexcept -> render_target&;
~render_target();
auto bind() const -> void;
auto unbind() const -> void;
auto color_id() const -> std::uint32_t;
auto width() const -> int;
auto height() const -> int;
auto valid() const -> bool;
auto resize(int width, int height) -> void;
private:
struct impl;
std::unique_ptr<impl> m_impl;
};
} // namespace cbt::gfx
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/buffer.hpp"
#include "glad/glad.h"
#include "cbt/opengl/buffer.hpp"
namespace cbt::opengl {
buffer::buffer() {
+15 -60
View File
@@ -1,27 +1,15 @@
#include <string_view>
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
#include "fmt/std.h"
#include "glad/glad.h"
#include "cbt/opengl/context.hpp"
#include <string_view>
#include "glad/glad.h"
#include "fmt/std.h"
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 {
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
fmt::print("Failed to initialize GLAD\n");
@@ -40,58 +28,25 @@ auto context::print_info() -> void {
fmt::print("\n");
}
context::context(std::string title, int width, int height) {
if (!init()) {
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);
context::context(window const& win) {
glfwMakeContextCurrent(win.raw());
if (!setup_gl()) {
terminate();
return;
}
m_valid = true;
// Set initial viewport to match window size
glViewport(0, 0, win.width(), win.height());
}
context::~context() {
if (m_window) {
glfwDestroyWindow(m_window);
}
if (m_initialized) {
terminate();
}
}
auto context::should_close() const -> bool {
return m_window && glfwWindowShouldClose(m_window);
}
context::~context() {}
auto context::valid() const -> bool {
return m_window != nullptr;
return m_valid;
}
auto context::swap_buffers() -> void {
glfwSwapBuffers(m_window);
}
auto context::poll_events() -> void {
glfwPollEvents();
}
auto context::raw() const -> GLFWwindow* {
return m_window;
auto context::set_size(int width, int height) -> void {
glViewport(0, 0, width, height);
}
}
+4 -13
View File
@@ -1,29 +1,20 @@
#pragma once
#include <string>
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
#include "cbt/window.hpp"
namespace cbt::opengl {
class context {
public:
explicit context(std::string title, int width, int height);
explicit context(window const& win);
~context();
auto should_close() const -> bool;
auto valid() const -> bool;
auto swap_buffers() -> void;
auto poll_events() -> void;
auto raw() const -> GLFWwindow*;
auto set_size(int width, int height) -> void;
private:
GLFWwindow* m_window = nullptr;
bool m_initialized = false;
bool m_valid = false;
static auto init() -> bool;
static auto terminate() -> void;
static auto setup_gl() -> bool;
static auto print_info() -> void;
};
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/descriptor.hpp"
#include "glad/glad.h"
#include "cbt/opengl/descriptor.hpp"
namespace cbt::opengl {
descriptor_set::descriptor_set() {}
+3 -3
View File
@@ -1,12 +1,12 @@
#pragma once
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/texture.hpp"
#include <array>
#include <optional>
#include <vector>
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/texture.hpp"
namespace cbt::opengl {
struct descriptor_binding {
+2 -2
View File
@@ -1,10 +1,10 @@
#include "cbt/opengl/shader.hpp"
#include <iostream>
#include <string>
#include "fmt/std.h"
#include "cbt/opengl/shader.hpp"
namespace cbt::opengl {
shader::shader() {}
+2 -2
View File
@@ -1,7 +1,7 @@
#include "cbt/opengl/texture.hpp"
#include "glad/glad.h"
#include "cbt/opengl/texture.hpp"
namespace cbt::opengl {
texture::texture() {
+3 -1
View File
@@ -8,6 +8,8 @@ auto scene::init() -> bool {
auto scene::update(float) -> void {}
auto scene::render() -> void {}
auto scene::render(int, int) -> void {}
auto scene::on_mouse_drag(double, double) -> void {}
}
+2 -1
View File
@@ -10,7 +10,8 @@ public:
virtual auto init() -> bool;
virtual auto update(float delta_time) -> void;
virtual auto render() -> void;
virtual auto render(int width, int height) -> void;
virtual auto on_mouse_drag(double dx, double dy) -> 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;
};
}
+122 -19
View File
@@ -1,58 +1,161 @@
#define GLFW_INCLUDE_NONE
#include "GLFW/glfw3.h"
#include "cbt/opengl/context.hpp"
#include "scenes/cube.hpp"
#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 {
auto ctx = cbt::opengl::context("cuber", 1280, 720);
#include "cbt/window.hpp"
#include "cbt/opengl/context.hpp"
#include "scenes/cube.hpp"
#include "scenes/sphere.hpp"
#include "scenes/cornell_box.hpp"
auto main(int argc, char const* argv[]) -> int {
float max_duration_seconds = 0.0f;
std::string scene_name = "cube"; // "cube", "sphere", "cornell_box", or "1"/"2"/"3"
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("Flags:\n");
fmt::print(" --duration <seconds> Auto-terminate after N seconds (for testing/CI)\n");
fmt::print(" --scene <cube|sphere|cornell_box> Select initial scene (default: cube)\n");
fmt::print(" --screenshot Render one frame, save screenshot, and exit\n");
fmt::print("\nKeys (during runtime):\n");
fmt::print(" S Take screenshot (saved as screenshot.png)\n");
fmt::print(" 1/2/3 Switch between cube/sphere/cornell_box scene\n");
fmt::print(" Q Quit\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]; // std::string to avoid string_view lifetime gotchas with argv
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()) {
return 1;
}
auto scn = cbt::scenes::cube();
if (!scn.init()) {
// Wire up resize callback
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();
auto cornell_box_scn = cbt::scenes::cornell_box();
cbt::scene* active_scene = nullptr;
if (!cube_scn.init() || !sphere_scn.init() || !cornell_box_scn.init()) {
return 1;
}
if (scene_name == "sphere" || scene_name == "2") {
active_scene = &sphere_scn;
} else if (scene_name == "cornell_box" || scene_name == "3") {
active_scene = &cornell_box_scn;
} else {
active_scene = &cube_scn;
}
// signal handling
// signal handling + optional duration timer (via ASIO)
asio::io_context io;
asio::signal_set signals(io, SIGINT, SIGTERM);
bool quit = false;
signals.async_wait([&](auto, auto) {
quit = true;
win.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 {
while (io.poll()) {}
};
// render loop
double mouse_x = 0.0, mouse_y = 0.0;
glfwGetCursorPos(win.raw(), &mouse_x, &mouse_y);
bool mouse_was_down = false;
auto prev = std::chrono::steady_clock::now();
while (!ctx.should_close()) {
while (!win.should_close()) {
process_signals();
if (quit || glfwGetKey(ctx.raw(), GLFW_KEY_Q) == GLFW_PRESS) {
break;
if (glfwGetKey(win.raw(), GLFW_KEY_Q) == GLFW_PRESS) {
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;
}
if (glfwGetKey(win.raw(), GLFW_KEY_3) == GLFW_PRESS) {
active_scene = &cornell_box_scn;
}
double cur_x = 0.0, cur_y = 0.0;
glfwGetCursorPos(win.raw(), &cur_x, &cur_y);
bool const mouse_down = glfwGetMouseButton(win.raw(), GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
if (mouse_down && mouse_was_down) {
active_scene->on_mouse_drag(cur_x - mouse_x, cur_y - mouse_y);
}
mouse_x = cur_x;
mouse_y = cur_y;
mouse_was_down = mouse_down;
auto now = std::chrono::steady_clock::now();
auto dt = std::chrono::duration<float>(now - prev).count();
prev = now;
scn.update(dt);
scn.render();
active_scene->update(dt);
active_scene->render(win.width(), win.height());
ctx.swap_buffers();
ctx.poll_events();
win.swap_buffers();
win.poll_events();
if (take_screenshot) {
win.screenshot();
win.stop();
}
}
return 0;
+20 -46
View File
@@ -1,28 +1,17 @@
# ==============================================================================
# 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:
# 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:
# stb_FOUND - TRUE if stb is available
# stb_LIBRARIES - The stb library target (stb::stb)
# stb_INCLUDE_DIR - Include directories for stb
# stb_VERSION - Version of stb (commit-based)
#
# 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.
# stb_VERSION - Version of stb (commit)
# ==============================================================================
if (DEFINED _FINDSTB_INCLUDED)
@@ -30,46 +19,31 @@ if (DEFINED _FINDSTB_INCLUDED)
endif()
set(_FINDSTB_INCLUDED TRUE)
# Use the version passed to find_package(), or default to 2.30
if (DEFINED stb_FIND_VERSION AND NOT stb_FIND_VERSION STREQUAL "")
set(STB_VERSION "${stb_FIND_VERSION}")
else()
set(STB_VERSION "2fb8c5a3deb2110c89669f8d6f36e5833b556b44")
endif()
# Pin to a recent stable commit
set(STB_VERSION "master")
message(STATUS "Fetching stb ${STB_VERSION}")
include(FetchContent)
find_program(GIT_EXECUTABLE git)
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
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG ${STB_VERSION}
)
endif()
FetchContent_Declare(
stb
GIT_REPOSITORY https://github.com/nothings/stb.git
GIT_TAG ${STB_VERSION}
)
FetchContent_MakeAvailable(stb)
if (NOT TARGET stb)
add_library(stb INTERFACE)
target_include_directories(stb SYSTEM INTERFACE "${stb_SOURCE_DIR}")
endif()
add_library(stb INTERFACE)
target_include_directories(stb INTERFACE "${stb_SOURCE_DIR}")
if (NOT TARGET stb::stb)
add_library(stb::stb ALIAS stb)
endif()
add_library(stb::stb ALIAS stb)
set(stb_FOUND TRUE)
set(stb_LIBRARIES stb::stb)
set(stb_VERSION "${STB_VERSION}")
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}"
)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+24
View File
@@ -0,0 +1,24 @@
add_library(scenes_cube STATIC "cube.cpp")
target_include_directories(scenes_cube PRIVATE "." "..")
target_compile_features(scenes_cube PRIVATE cxx_std_23)
target_compile_options(scenes_cube PRIVATE ${BASE_OPTIONS})
target_compile_definitions(scenes_cube PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(scenes_cube PUBLIC cbt_scene glm::glm)
add_library(scenes_sphere STATIC "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)
add_library(scenes_cornell_box STATIC "cornell_box.cpp")
target_include_directories(scenes_cornell_box PRIVATE "." "..")
target_compile_features(scenes_cornell_box PRIVATE cxx_std_23)
target_compile_options(scenes_cornell_box PRIVATE ${BASE_OPTIONS})
target_compile_definitions(scenes_cornell_box PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(scenes_cornell_box PUBLIC cbt_scene glm::glm)
# Convenience interface for all scenes
add_library(scenes INTERFACE)
target_link_libraries(scenes INTERFACE scenes_cube scenes_sphere scenes_cornell_box)
+300
View File
@@ -0,0 +1,300 @@
#include <cstddef>
#include <cmath>
#include <vector>
#include <span>
#include <array>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "scenes/cornell_box.hpp"
namespace cbt::scenes {
auto cornell_box::init() -> bool {
return build_scene_pipeline() && build_light_pipeline() && build_post_pipeline();
}
auto cornell_box::update(float) -> void {}
auto cornell_box::on_mouse_drag(double dx, double dy) -> void {
float const sensitivity = 0.3f;
m_yaw += float(dx) * sensitivity;
m_pitch += float(dy) * sensitivity;
m_pitch = glm::clamp(m_pitch, -80.0f, 80.0f);
}
auto cornell_box::render(int width, int height) -> void {
m_rt.resize(width, height);
glViewport(0, 0, width, height);
// Offscreen pass
m_rt.bind();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
float const aspect = float(width) / float(height);
float const yaw_rad = glm::radians(m_yaw);
float const pitch_rad = glm::radians(m_pitch);
glm::vec3 const eye{
m_radius * sinf(yaw_rad) * cosf(pitch_rad),
m_radius * sinf(pitch_rad),
m_radius * cosf(yaw_rad) * cosf(pitch_rad),
};
auto proj = glm::perspective(glm::radians(40.0f), aspect, 0.1f, 20.0f);
auto view = glm::lookAt(eye, glm::vec3{0.0f}, glm::vec3{0.0f, 1.0f, 0.0f});
m_scene_pipeline.bind_vec3("u_light_pos", {0.0f, 0.9f, 0.0f});
m_scene_pipeline.bind_vec3("u_light_color", {1.0f, 1.0f, 1.0f});
m_scene_pipeline.draw(glm::mat4{1.0f}, view, proj);
m_light_pipeline.draw(glm::mat4{1.0f}, view, proj);
m_rt.unbind();
// Screen pass
glViewport(0, 0, width, height);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
m_post_pipeline.bind_texture("u_texture", m_rt.color_id(), 0);
m_post_pipeline.draw(glm::mat4{1.0f}, glm::mat4{1.0f}, glm::mat4{1.0f});
}
auto cornell_box::build_scene_pipeline() -> 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 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 vec3 v_color;
void main() {
gl_Position = u_proj * u_view * u_model * vec4(a_pos, 1.0);
v_normal = a_normal;
v_world_pos = a_pos;
v_color = a_color;
}
)glsl";
char const* frag_src = R"glsl(
#version 410 core
in vec3 v_normal;
in vec3 v_world_pos;
in vec3 v_color;
uniform vec3 u_light_pos;
uniform vec3 u_light_color;
out vec4 frag_color;
void main() {
vec3 n = normalize(v_normal);
vec3 l = normalize(u_light_pos - v_world_pos);
float diff = max(dot(n, l), 0.0);
vec3 ambient = 0.12 * v_color;
vec3 diffuse = diff * 0.88 * v_color * u_light_color;
frag_color = vec4(ambient + diffuse, 1.0);
}
)glsl";
struct vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec3 color;
};
glm::vec3 const white{0.73f, 0.71f, 0.68f};
glm::vec3 const red{0.65f, 0.05f, 0.05f};
glm::vec3 const green{0.12f, 0.45f, 0.09f};
std::vector<vertex> verts;
std::vector<std::uint32_t> inds;
auto add_quad = [&](glm::vec3 p0, glm::vec3 p1, glm::vec3 p2, glm::vec3 p3,
glm::vec3 normal, glm::vec3 color) {
auto base = static_cast<std::uint32_t>(verts.size());
verts.push_back({p0, normal, color});
verts.push_back({p1, normal, color});
verts.push_back({p2, normal, color});
verts.push_back({p3, normal, color});
inds.insert(inds.end(), {base, base + 1, base + 2, base, base + 2, base + 3});
};
// Room walls
add_quad({-1,-1, 1}, { 1,-1, 1}, { 1,-1,-1}, {-1,-1,-1}, { 0, 1, 0}, white); // floor
add_quad({-1, 1,-1}, { 1, 1,-1}, { 1, 1, 1}, {-1, 1, 1}, { 0,-1, 0}, white); // ceiling
add_quad({-1,-1,-1}, { 1,-1,-1}, { 1, 1,-1}, {-1, 1,-1}, { 0, 0, 1}, white); // back
add_quad({-1,-1, 1}, {-1, 1, 1}, {-1, 1,-1}, {-1,-1,-1}, { 1, 0, 0}, red); // left
add_quad({ 1,-1,-1}, { 1, 1,-1}, { 1, 1, 1}, { 1,-1, 1}, {-1, 0, 0}, green); // right
// Boxes: vertices are pre-transformed into world space so the model matrix
// stays identity and normals need no runtime adjustment.
auto add_box = [&](glm::vec3 center, glm::vec3 half, float y_deg, glm::vec3 color) {
float const rad = glm::radians(y_deg);
float const cy = cosf(rad);
float const sy = sinf(rad);
auto ry = [&](glm::vec3 v) -> glm::vec3 {
return {v.x * cy + v.z * sy, v.y, -v.x * sy + v.z * cy};
};
glm::vec3 c[8] = {
center + ry({-half.x, -half.y, -half.z}),
center + ry({ half.x, -half.y, -half.z}),
center + ry({ half.x, half.y, -half.z}),
center + ry({-half.x, half.y, -half.z}),
center + ry({-half.x, -half.y, half.z}),
center + ry({ half.x, -half.y, half.z}),
center + ry({ half.x, half.y, half.z}),
center + ry({-half.x, half.y, half.z}),
};
struct face_def { int vi[4]; glm::vec3 n; };
face_def const faces[6] = {
{{4, 5, 6, 7}, { 0, 0, 1}}, // +Z
{{1, 0, 3, 2}, { 0, 0, -1}}, // -Z
{{0, 4, 7, 3}, {-1, 0, 0}}, // -X
{{5, 1, 2, 6}, { 1, 0, 0}}, // +X
{{7, 6, 2, 3}, { 0, 1, 0}}, // +Y
{{0, 1, 5, 4}, { 0, -1, 0}}, // -Y
};
for (auto const& f : faces) {
glm::vec3 const normal = ry(f.n);
auto const base = static_cast<std::uint32_t>(verts.size());
for (int i = 0; i < 4; ++i) {
verts.push_back({c[f.vi[i]], normal, color});
}
inds.insert(inds.end(), {base, base + 1, base + 2, base, base + 2, base + 3});
}
};
// Proportions from the original Cornell Box paper (room = 555 units → [-1, 1]).
// Tall box: 165×330×165 mm, center at (185, 165, 169), rotated +18°.
add_box({-0.33f, -0.40f, -0.39f}, {0.30f, 0.60f, 0.30f}, 18.0f, white);
// Short box: 165×165×165 mm, center at (370, 82.5, 351), rotated -15°.
add_box({ 0.33f, -0.70f, 0.27f}, {0.30f, 0.30f, 0.30f}, -15.0f, white);
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{verts}),
.index_data = std::as_bytes(std::span{inds}),
.attributes = {
{.location = 0, .num_components = 3, .offset = 0},
{.location = 1, .num_components = 3, .offset = 12},
{.location = 2, .num_components = 3, .offset = 24},
},
.vertex_stride = sizeof(vertex),
.vertex_shader_src = vert_src,
.fragment_shader_src = frag_src,
.depth_test = true,
.primitive = gfx::primitive_type::triangles,
.index_type_ = gfx::index_type::uint32
};
m_scene_pipeline = gfx::pipeline{desc};
return m_scene_pipeline.valid();
}
auto cornell_box::build_light_pipeline() -> bool {
char const* vert_src = R"glsl(
#version 410 core
layout(location = 0) in vec3 a_pos;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_proj;
void main() {
gl_Position = u_proj * u_view * u_model * vec4(a_pos, 1.0);
}
)glsl";
char const* frag_src = R"glsl(
#version 410 core
out vec4 frag_color;
void main() {
frag_color = vec4(1.5, 1.4, 1.2, 1.0);
}
)glsl";
struct pos_vertex { glm::vec3 position; };
// Ceiling light panel inset slightly from y=1 to avoid z-fighting with ceiling.
std::array<pos_vertex, 4> const light_verts = {{
{{-0.35f, 0.995f, -0.35f}},
{{ 0.35f, 0.995f, -0.35f}},
{{ 0.35f, 0.995f, 0.35f}},
{{-0.35f, 0.995f, 0.35f}},
}};
std::array<std::uint32_t, 6> const light_inds = {0, 1, 2, 0, 2, 3};
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{light_verts}),
.index_data = std::as_bytes(std::span{light_inds}),
.attributes = {
{.location = 0, .num_components = 3, .offset = 0},
},
.vertex_stride = sizeof(pos_vertex),
.vertex_shader_src = vert_src,
.fragment_shader_src = frag_src,
.depth_test = true,
.primitive = gfx::primitive_type::triangles,
.index_type_ = gfx::index_type::uint32
};
m_light_pipeline = gfx::pipeline{desc};
return m_light_pipeline.valid();
}
auto cornell_box::build_post_pipeline() -> bool {
char const* post_vert = R"glsl(
#version 410 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_uv;
out vec2 v_uv;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
v_uv = a_uv;
}
)glsl";
char const* post_frag = R"glsl(
#version 410 core
in vec2 v_uv;
uniform sampler2D u_texture;
out vec4 frag_color;
void main() {
vec3 col = texture(u_texture, v_uv).rgb;
float vig = 1.0 - length(v_uv * 2.0 - 1.0) * 0.35;
frag_color = vec4(col * vig, 1.0);
}
)glsl";
struct fs_vertex { glm::vec2 pos; glm::vec2 uv; };
std::array<fs_vertex, 4> const qverts = {{
{{-1.0f, -1.0f}, {0.0f, 0.0f}},
{{ 1.0f, -1.0f}, {1.0f, 0.0f}},
{{ 1.0f, 1.0f}, {1.0f, 1.0f}},
{{-1.0f, 1.0f}, {0.0f, 1.0f}},
}};
std::array<std::uint32_t, 6> const qinds = {0, 1, 2, 0, 2, 3};
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{qverts}),
.index_data = std::as_bytes(std::span{qinds}),
.attributes = {
{.location = 0, .num_components = 2, .offset = 0},
{.location = 1, .num_components = 2, .offset = 8},
},
.vertex_stride = sizeof(fs_vertex),
.vertex_shader_src = post_vert,
.fragment_shader_src = post_frag,
.depth_test = false,
.primitive = gfx::primitive_type::triangles,
.index_type_ = gfx::index_type::uint32
};
m_post_pipeline = gfx::pipeline{desc};
return m_post_pipeline.valid();
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/gfx.hpp"
namespace cbt::scenes {
class cornell_box final : public scene {
public:
auto init() -> bool override;
auto update(float delta_time) -> void override;
auto render(int width, int height) -> void override;
auto on_mouse_drag(double dx, double dy) -> void override;
private:
gfx::pipeline m_scene_pipeline;
gfx::pipeline m_light_pipeline;
gfx::pipeline m_post_pipeline;
gfx::render_target m_rt{0, 0};
float m_yaw = 0.0f; // degrees, horizontal orbit
float m_pitch = 0.0f; // degrees, vertical orbit
float m_radius = 4.0f;
auto build_scene_pipeline() -> bool;
auto build_light_pipeline() -> bool;
auto build_post_pipeline() -> bool;
};
}
+49 -65
View File
@@ -1,11 +1,10 @@
#include "scenes/cube.hpp"
#include <array>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <span>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "scenes/cube.hpp"
namespace cbt::scenes {
@@ -14,40 +13,55 @@ cube::cube() {
}
auto cube::init() -> bool {
if (!build_shader()) {
if (!build_pipeline()) {
return false;
}
build_mesh();
glEnable(GL_DEPTH_TEST);
return true;
}
auto cube::update(float) -> void {}
auto cube::render() -> void {
auto cube::render(int width, int height) -> void {
glViewport(0, 0, width, height);
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 = 1280.0f / 720.0f;
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, -3.0f});
auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.5f, 0.3f});
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();
glDrawArrays(GL_TRIANGLES, 0, 36);
m_vao.unbind();
m_prog.unuse();
m_pipeline.draw(model, view, proj);
}
auto cube::build_mesh() -> void {
auto cube::build_pipeline() -> bool {
char const* vert_src = R"glsl(
#version 410 core
layout(location = 0) in vec3 a_pos;
layout(location = 1) in vec3 a_color;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_proj;
out vec3 v_color;
void main() {
gl_Position = u_proj * u_view * u_model * vec4(a_pos, 1.0);
v_color = a_color;
}
)glsl";
char const* frag_src = R"glsl(
#version 410 core
in vec3 v_color;
out vec4 frag_color;
void main() {
frag_color = vec4(v_color, 1.0);
}
)glsl";
std::array<float, 36 * 6> vertices = {
// front face (cyan)
0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 1.0f,
@@ -98,52 +112,22 @@ auto cube::build_mesh() -> void {
-0.5f, 0.5f, -0.5f, 1.0f, 0.5f, 0.0f,
};
m_vbo.upload(vertices.data(), vertices.size() * sizeof(float));
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{vertices}),
.attributes = {
{.location = 0, .num_components = 3, .offset = 0},
{.location = 1, .num_components = 3, .offset = 12},
},
.vertex_stride = 24,
.vertex_shader_src = vert_src,
.fragment_shader_src = frag_src,
.depth_test = true,
.primitive = gfx::primitive_type::triangles
// no index data
};
m_vao.bind();
m_vbo.bind();
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), nullptr);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float),
reinterpret_cast<void*>(3 * sizeof(float)));
m_vbo.unbind();
m_vao.unbind();
}
auto cube::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_color;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_proj;
out vec3 v_color;
void main() {
gl_Position = u_proj * u_view * u_model * vec4(a_pos, 1.0);
v_color = a_color;
}
)glsl";
char const* frag_src = R"glsl(
#version 410 core
in vec3 v_color;
out vec4 frag_color;
void main() {
frag_color = vec4(v_color, 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;
m_pipeline = gfx::pipeline{desc};
return m_pipeline.valid();
}
}
+8 -16
View File
@@ -1,12 +1,11 @@
#pragma once
#include <chrono>
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/vao.hpp"
#include <glm/glm.hpp>
#include "cbt/gfx.hpp"
namespace cbt::scenes {
@@ -15,21 +14,14 @@ public:
cube();
auto init() -> bool override;
auto update(float delta_time) -> void override;
auto render() -> void override;
auto render(int width, int height) -> void override;
private:
opengl::shader m_prog;
opengl::buffer m_vbo;
opengl::vao m_vao;
GLint m_loc_proj = -1;
GLint m_loc_view = -1;
GLint m_loc_model = -1;
gfx::pipeline m_pipeline;
std::chrono::steady_clock::time_point m_start;
auto build_mesh() -> void;
auto build_shader() -> bool;
auto build_pipeline() -> bool;
};
}
+254
View File
@@ -0,0 +1,254 @@
#include <cstddef>
#include <cmath>
#include <vector>
#include <span>
#include <array>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "scenes/sphere.hpp"
namespace cbt::scenes {
sphere::sphere() {
m_start = std::chrono::steady_clock::now();
}
auto sphere::init() -> bool {
if (!build_pipeline() || !build_post_pipeline()) {
return false;
}
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();
m_rt.resize(width, height);
glViewport(0, 0, width, height);
// Step 1: Render scene to texture (offscreen pass)
m_rt.bind();
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_scene_pipeline.draw(model, view, proj);
m_rt.unbind();
// Reset viewport for screen pass (RT bind changed it)
glViewport(0, 0, width, height);
// Step 2: Post-processing step (sample RT texture, apply effect, render to screen)
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
m_post_pipeline.bind_texture("u_texture", m_rt.color_id(), 0);
m_post_pipeline.draw(glm::mat4{1.0f}, glm::mat4{1.0f}, glm::mat4{1.0f});
}
auto sphere::build_pipeline() -> 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";
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;
}
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{vertices}),
.index_data = std::as_bytes(std::span{indices}),
.attributes = {
{.location = 0, .num_components = 3, .offset = 0},
{.location = 1, .num_components = 3, .offset = 12},
{.location = 2, .num_components = 2, .offset = 24},
{.location = 3, .num_components = 3, .offset = 32},
},
.vertex_stride = sizeof(vertex),
.vertex_shader_src = vert_src,
.fragment_shader_src = frag_src,
.depth_test = true,
.primitive = gfx::primitive_type::triangles,
.index_type_ = gfx::index_type::uint32
};
m_scene_pipeline = gfx::pipeline{desc};
return m_scene_pipeline.valid();
}
auto sphere::build_post_pipeline() -> bool {
char const* post_vert = R"glsl(
#version 410 core
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_uv;
out vec2 v_uv;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
v_uv = a_uv;
}
)glsl";
char const* post_frag = R"glsl(
#version 410 core
in vec2 v_uv;
uniform sampler2D u_texture;
out vec4 frag_color;
void main() {
vec3 col = texture(u_texture, v_uv).rgb;
// simple processing: vignette only (preserves original colors from sphere faces)
float vig = 1.0 - length(v_uv * 2.0 - 1.0) * 0.5;
frag_color = vec4(col * vig, 1.0);
}
)glsl";
struct fs_vertex {
glm::vec2 pos;
glm::vec2 uv;
};
std::array<fs_vertex, 4> qverts = {{
{{-1.0f, -1.0f}, {0.0f, 0.0f}},
{{ 1.0f, -1.0f}, {1.0f, 0.0f}},
{{ 1.0f, 1.0f}, {1.0f, 1.0f}},
{{-1.0f, 1.0f}, {0.0f, 1.0f}},
}};
std::array<std::uint32_t, 6> qinds = {0, 1, 2, 0, 2, 3};
gfx::pipeline_desc desc{
.vertex_data = std::as_bytes(std::span{qverts}),
.index_data = std::as_bytes(std::span{qinds}),
.attributes = {
{.location = 0, .num_components = 2, .offset = 0},
{.location = 1, .num_components = 2, .offset = 8},
},
.vertex_stride = sizeof(fs_vertex),
.vertex_shader_src = post_vert,
.fragment_shader_src = post_frag,
.depth_test = false,
.primitive = gfx::primitive_type::triangles,
.index_type_ = gfx::index_type::uint32
};
m_post_pipeline = gfx::pipeline{desc};
return m_post_pipeline.valid();
}
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <chrono>
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/gfx.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:
gfx::pipeline m_scene_pipeline;
gfx::pipeline m_post_pipeline;
gfx::render_target m_rt{0, 0};
std::chrono::steady_clock::time_point m_start;
auto build_pipeline() -> bool;
auto build_post_pipeline() -> bool;
};
}