Compare commits
3 Commits
c83561c0fa
...
trunk
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bfde6c6fb | |||
| 5b4743ff8f | |||
| 6c1096fbb8 |
@@ -12,11 +12,11 @@
|
||||
|
||||
### Commands
|
||||
|
||||
```sh
|
||||
cmake -S . -B build -GNinja
|
||||
ninja -C build
|
||||
./build/cuber
|
||||
```
|
||||
```sh
|
||||
cmake -S . -B build -GNinja
|
||||
ninja -C build
|
||||
./build/cuber
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -34,10 +34,13 @@ To add a new dependency:
|
||||
|
||||
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
|
||||
- **`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
|
||||
|
||||
@@ -52,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:
|
||||
@@ -82,21 +88,35 @@ 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
|
||||
@@ -123,6 +143,8 @@ cuber/
|
||||
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
|
||||
```
|
||||
|
||||
+2
-20
@@ -46,25 +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)
|
||||
|
||||
# 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)
|
||||
add_subdirectory(scenes)
|
||||
|
||||
# Main executable
|
||||
add_executable(cuber "cuber.cpp")
|
||||
@@ -72,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 scenes_sphere asio::asio ${BASE_LIBRARIES})
|
||||
target_link_libraries(cuber PRIVATE cbt_scene scenes asio::asio ${BASE_LIBRARIES})
|
||||
|
||||
@@ -48,4 +48,112 @@ Q key Quit
|
||||
|
||||
- **cube** — spinning colored cube with per-face colors
|
||||
- **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
@@ -34,6 +34,8 @@ struct pipeline::impl {
|
||||
|
||||
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 {
|
||||
@@ -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 {
|
||||
if (!m_prog.valid()) {
|
||||
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 {
|
||||
GLuint m_fbo = 0;
|
||||
opengl::texture m_color{opengl::texture_target::_2d};
|
||||
|
||||
@@ -50,6 +50,8 @@ public:
|
||||
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;
|
||||
|
||||
@@ -10,4 +10,6 @@ auto scene::update(float) -> void {}
|
||||
|
||||
auto scene::render(int, int) -> void {}
|
||||
|
||||
auto scene::on_mouse_drag(double, double) -> void {}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public:
|
||||
virtual auto init() -> bool;
|
||||
virtual auto update(float delta_time) -> void;
|
||||
virtual auto render(int width, int height) -> void;
|
||||
virtual auto on_mouse_drag(double dx, double dy) -> void;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -14,21 +14,25 @@
|
||||
#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_view scene_name = "cube";
|
||||
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> 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(" S key Take screenshot (saved as screenshot.png)\n");
|
||||
fmt::print(" 1/2 key Switch between cube/sphere scene\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) {
|
||||
@@ -36,7 +40,7 @@ auto main(int argc, char const* argv[]) -> int {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (arg == "--screenshot") {
|
||||
@@ -62,15 +66,18 @@ auto main(int argc, char const* argv[]) -> int {
|
||||
ctx.set_size(width, height);
|
||||
});
|
||||
|
||||
auto cube_scn = cbt::scenes::cube();
|
||||
auto sphere_scn = cbt::scenes::sphere();
|
||||
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()) {
|
||||
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;
|
||||
}
|
||||
@@ -100,6 +107,10 @@ auto main(int argc, char const* argv[]) -> int {
|
||||
};
|
||||
|
||||
// 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 (!win.should_close()) {
|
||||
@@ -117,6 +128,19 @@ auto main(int argc, char const* argv[]) -> int {
|
||||
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();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 148 KiB |
@@ -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)
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user