Compare commits

...

3 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
12 changed files with 576 additions and 45 deletions
+38 -16
View File
@@ -12,11 +12,11 @@
### Commands ### Commands
```sh ```sh
cmake -S . -B build -GNinja cmake -S . -B build -GNinja
ninja -C build ninja -C build
./build/cuber ./build/cuber
``` ```
### Dependencies ### Dependencies
@@ -34,10 +34,13 @@ To add a new dependency:
The project is split into static libraries: The project is split into static libraries:
- **`cbt_opengl`** — OpenGL abstraction and window management (window, 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`** — Spinning cube scene implementation - **`scenes_cube`** — Spinning cube scene implementation
- **`scenes_sphere`** — Cube-to-sphere mapped mesh with diffuse lighting - **`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 ### CMake Module Path
@@ -52,10 +55,13 @@ to the custom scripts instead of system-installed packages.
- **No semicolons after closing braces** for namespaces/classes - **No semicolons after closing braces** for namespaces/classes
- `auto` for obvious types (e.g. `auto main(...) -> int`) - `auto` for obvious types (e.g. `auto main(...) -> int`)
- **East const** (e.g. `char const*` not `const char*`) - **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&`) - **Trailing return type** for all function definitions, including
- **Public members first** in class declarations, private members at the bottom 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 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:** `snake_case` for variables, functions, and classes
- **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants - **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants
- Include order: - Include order:
@@ -82,21 +88,35 @@ to the custom scripts instead of system-installed packages.
- Follow the 50/72 rule: - Follow the 50/72 rule:
- Subject line: max 50 characters - Subject line: max 50 characters
- Body lines: wrapped at 72 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 - Separate subject from body with a blank line
- Do **not** add a `Co-Authored-By` trailer or any agent/AI attribution
Example: Example:
``` ```
feat: add stopwatch timer feat: add stopwatch timer
Replace Hello World with a live stopwatch that prints elapsed time Replace Hello World with a live stopwatch that prints elapsed time
in HH:MM:SS.mmm format, updating every 10ms with color output. 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 ## Source Layout
``` ```text
cuber/ cuber/
CMakeLists.txt # Build configuration CMakeLists.txt # Build configuration
cuber.cpp # Entry point cuber.cpp # Entry point
@@ -123,6 +143,8 @@ cuber/
cube.cpp # Cube scene implementation cube.cpp # Cube scene implementation
sphere.hpp # Cube-to-sphere mapped mesh sphere.hpp # Cube-to-sphere mapped mesh
sphere.cpp # Sphere scene implementation sphere.cpp # Sphere scene implementation
cornell_box.hpp # Cornell Box scene
cornell_box.cpp # Cornell Box scene implementation
deps/ # Custom Find*.cmake scripts deps/ # Custom Find*.cmake scripts
Findfmt.cmake # fmt library Findfmt.cmake # fmt library
``` ```
+2 -20
View File
@@ -46,25 +46,7 @@ target_compile_options(cbt_scene PRIVATE ${BASE_OPTIONS})
target_compile_definitions(cbt_scene PRIVATE ${BASE_DEFINITIONS}) target_compile_definitions(cbt_scene PRIVATE ${BASE_DEFINITIONS})
target_link_libraries(cbt_scene PUBLIC cbt_opengl) target_link_libraries(cbt_scene PUBLIC cbt_opengl)
# Application scenes add_subdirectory(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)
# 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")
@@ -72,4 +54,4 @@ 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 scenes_sphere asio::asio ${BASE_LIBRARIES}) target_link_libraries(cuber PRIVATE cbt_scene scenes asio::asio ${BASE_LIBRARIES})
+109 -1
View File
@@ -48,4 +48,112 @@ Q key Quit
- **cube** — spinning colored cube with per-face colors - **cube** — spinning colored cube with per-face colors
- **sphere** — cube-to-sphere mapped mesh with per-face colors and - **sphere** — cube-to-sphere mapped mesh with per-face colors and
diffuse lighting 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)
+34
View File
@@ -34,6 +34,8 @@ struct pipeline::impl {
auto build(pipeline_desc const& desc) -> bool; 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_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 { auto pipeline::impl::build(pipeline_desc const& desc) -> bool {
@@ -105,6 +107,26 @@ auto pipeline::impl::build(pipeline_desc const& desc) -> bool {
} }
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 { auto pipeline::impl::bind_texture(char const* sampler_name, std::uint32_t texture_id, std::uint32_t unit) const -> void {
if (!m_prog.valid()) { if (!m_prog.valid()) {
return; return;
@@ -187,6 +209,18 @@ auto pipeline::bind_texture(char const* sampler_name, std::uint32_t texture_id,
} }
} }
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 { struct render_target::impl {
GLuint m_fbo = 0; GLuint m_fbo = 0;
opengl::texture m_color{opengl::texture_target::_2d}; opengl::texture m_color{opengl::texture_target::_2d};
+2
View File
@@ -50,6 +50,8 @@ public:
auto valid() const -> bool; auto valid() const -> bool;
auto draw(glm::mat4 const& model, glm::mat4 const& view, glm::mat4 const& proj) const -> void; 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_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: private:
struct impl; struct impl;
+2
View File
@@ -10,4 +10,6 @@ auto scene::update(float) -> void {}
auto scene::render(int, int) -> void {} auto scene::render(int, int) -> void {}
auto scene::on_mouse_drag(double, double) -> void {}
} }
+1
View File
@@ -11,6 +11,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(int width, int height) -> void; virtual auto render(int width, int height) -> void;
virtual auto on_mouse_drag(double dx, double dy) -> void;
}; };
} }
+32 -8
View File
@@ -14,21 +14,25 @@
#include "cbt/opengl/context.hpp" #include "cbt/opengl/context.hpp"
#include "scenes/cube.hpp" #include "scenes/cube.hpp"
#include "scenes/sphere.hpp" #include "scenes/sphere.hpp"
#include "scenes/cornell_box.hpp"
auto main(int argc, char const* argv[]) -> int { auto main(int argc, char const* argv[]) -> int {
float max_duration_seconds = 0.0f; float max_duration_seconds = 0.0f;
std::string_view scene_name = "cube"; std::string scene_name = "cube"; // "cube", "sphere", "cornell_box", or "1"/"2"/"3"
bool take_screenshot = false; bool take_screenshot = false;
for (int i = 1; i < argc; ++i) { for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i]; std::string_view arg = argv[i];
if (arg == "--help" || arg == "-h") { if (arg == "--help" || arg == "-h") {
fmt::print("Usage: {} [options]\n", argv[0]); 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(" --duration <seconds> Auto-terminate after N seconds (for testing/CI)\n");
fmt::print(" --scene <cube|sphere> Select initial scene (default: cube)\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(" --screenshot Render one frame, save screenshot, and exit\n");
fmt::print(" S key Take screenshot (saved as screenshot.png)\n"); fmt::print("\nKeys (during runtime):\n");
fmt::print(" 1/2 key Switch between cube/sphere scene\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; return 0;
} }
if (arg == "--duration" && i + 1 < argc) { if (arg == "--duration" && i + 1 < argc) {
@@ -36,7 +40,7 @@ auto main(int argc, char const* argv[]) -> int {
continue; continue;
} }
if (arg == "--scene" && i + 1 < argc) { if (arg == "--scene" && i + 1 < argc) {
scene_name = argv[++i]; scene_name = argv[++i]; // std::string to avoid string_view lifetime gotchas with argv
continue; continue;
} }
if (arg == "--screenshot") { if (arg == "--screenshot") {
@@ -62,15 +66,18 @@ auto main(int argc, char const* argv[]) -> int {
ctx.set_size(width, height); ctx.set_size(width, height);
}); });
auto cube_scn = cbt::scenes::cube(); auto cube_scn = cbt::scenes::cube();
auto sphere_scn = cbt::scenes::sphere(); auto sphere_scn = cbt::scenes::sphere();
auto cornell_box_scn = cbt::scenes::cornell_box();
cbt::scene* active_scene = nullptr; cbt::scene* active_scene = nullptr;
if (!cube_scn.init() || !sphere_scn.init()) { if (!cube_scn.init() || !sphere_scn.init() || !cornell_box_scn.init()) {
return 1; return 1;
} }
if (scene_name == "sphere" || scene_name == "2") { if (scene_name == "sphere" || scene_name == "2") {
active_scene = &sphere_scn; active_scene = &sphere_scn;
} else if (scene_name == "cornell_box" || scene_name == "3") {
active_scene = &cornell_box_scn;
} else { } else {
active_scene = &cube_scn; active_scene = &cube_scn;
} }
@@ -100,6 +107,10 @@ auto main(int argc, char const* argv[]) -> int {
}; };
// render loop // 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(); auto prev = std::chrono::steady_clock::now();
while (!win.should_close()) { while (!win.should_close()) {
@@ -117,6 +128,19 @@ auto main(int argc, char const* argv[]) -> int {
if (glfwGetKey(win.raw(), GLFW_KEY_2) == GLFW_PRESS) { if (glfwGetKey(win.raw(), GLFW_KEY_2) == GLFW_PRESS) {
active_scene = &sphere_scn; 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 now = std::chrono::steady_clock::now();
auto dt = std::chrono::duration<float>(now - prev).count(); auto dt = std::chrono::duration<float>(now - prev).count();
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

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;
};
}