Compare commits

...

5 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
17 changed files with 1143 additions and 211 deletions
-3
View File
@@ -43,6 +43,3 @@ imgui.ini
# Compiled shaders
*.spv
# Screenshots
*.png
+38 -16
View File
@@ -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
```
+4 -21
View File
@@ -28,12 +28,13 @@ add_library(cbt_opengl STATIC
"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 stb::stb)
target_link_libraries(cbt_opengl PUBLIC fmt::fmt glfw::glfw glad::glad stb::stb glm::glm)
# Scene base library
add_library(cbt_scene STATIC
@@ -45,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")
@@ -71,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})
+111 -1
View File
@@ -2,6 +2,8 @@
`cuber` is an OpenGL 3D renderer with multiple scenes.
![Screenshot of sphere](./res/preview.png)
## Requirements
- CMake 3.21+
@@ -46,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)
+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
View File
@@ -10,4 +10,6 @@ auto scene::update(float) -> 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 update(float delta_time) -> void;
virtual auto render(int width, int height) -> void;
virtual auto on_mouse_drag(double dx, double dy) -> void;
};
}
+30 -6
View File
@@ -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") {
@@ -64,13 +68,16 @@ auto main(int argc, char const* argv[]) -> int {
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();
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;
};
}
+44 -59
View File
@@ -1,8 +1,8 @@
#include <array>
#include <span>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "scenes/cube.hpp"
@@ -13,17 +13,17 @@ 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(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();
@@ -35,18 +35,33 @@ auto cube::render(int width, int height) -> void {
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,
@@ -97,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();
}
}
+3 -12
View File
@@ -5,9 +5,7 @@
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/vao.hpp"
#include "cbt/gfx.hpp"
namespace cbt::scenes {
@@ -19,18 +17,11 @@ public:
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;
};
}
+116 -76
View File
@@ -1,10 +1,11 @@
#include <cstddef>
#include <cmath>
#include <vector>
#include <span>
#include <array>
#include "glad/glad.h"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "scenes/sphere.hpp"
@@ -15,11 +16,9 @@ sphere::sphere() {
}
auto sphere::init() -> bool {
if (!build_shader()) {
if (!build_pipeline() || !build_post_pipeline()) {
return false;
}
build_mesh();
glEnable(GL_DEPTH_TEST);
return true;
}
@@ -29,6 +28,12 @@ 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);
@@ -37,18 +42,59 @@ auto sphere::render(int width, int height) -> void {
auto view = glm::translate(glm::mat4{1.0f}, glm::vec3{0.0f, 0.0f, -4.0f});
auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.3f, 0.2f});
m_prog.use();
glUniformMatrix4fv(m_loc_proj, 1, GL_FALSE, glm::value_ptr(proj));
glUniformMatrix4fv(m_loc_view, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(m_loc_model, 1, GL_FALSE, glm::value_ptr(model));
m_scene_pipeline.draw(model, view, proj);
m_rt.unbind();
m_vao.bind();
glDrawElements(GL_TRIANGLES, m_index_count, GL_UNSIGNED_INT, nullptr);
m_vao.unbind();
m_prog.unuse();
// 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_mesh() -> void {
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;
@@ -127,88 +173,82 @@ auto sphere::build_mesh() -> void {
offset += div * div;
}
m_index_count = static_cast<GLsizei>(indices.size());
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_vbo.upload(vertices.data(), vertices.size() * sizeof(vertex));
m_ebo.upload(indices.data(), indices.size() * sizeof(std::uint32_t));
m_vao.bind();
m_vbo.bind();
m_ebo.bind();
// location 0: position (vec3)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), nullptr);
// location 1: normal (vec3)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(3 * sizeof(float)));
// location 2: uv (vec2)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(6 * sizeof(float)));
// location 3: color (vec3)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(vertex),
reinterpret_cast<void*>(8 * sizeof(float)));
m_vao.unbind();
m_vbo.unbind();
m_ebo.unbind();
m_scene_pipeline = gfx::pipeline{desc};
return m_scene_pipeline.valid();
}
auto sphere::build_shader() -> bool {
char const* vert_src = R"glsl(
auto sphere::build_post_pipeline() -> bool {
char const* post_vert = 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;
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_uv;
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;
gl_Position = vec4(a_pos, 0.0, 1.0);
v_uv = a_uv;
v_color = a_color;
}
)glsl";
char const* frag_src = R"glsl(
char const* post_frag = R"glsl(
#version 410 core
in vec3 v_normal;
in vec3 v_world_pos;
in vec2 v_uv;
in vec3 v_color;
uniform sampler2D u_texture;
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);
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";
if (!m_prog.compile_vertex(vert_src) || !m_prog.compile_fragment(frag_src) || !m_prog.link()) {
return false;
}
struct fs_vertex {
glm::vec2 pos;
glm::vec2 uv;
};
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");
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};
return true;
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();
}
}
+6 -15
View File
@@ -5,9 +5,7 @@
#include "glm/glm.hpp"
#include "cbt/scene.hpp"
#include "cbt/opengl/buffer.hpp"
#include "cbt/opengl/shader.hpp"
#include "cbt/opengl/vao.hpp"
#include "cbt/gfx.hpp"
namespace cbt::scenes {
@@ -19,21 +17,14 @@ public:
auto render(int width, int height) -> void override;
private:
opengl::shader m_prog;
opengl::buffer m_vbo;
opengl::buffer m_ebo{opengl::buffer_type::index};
opengl::vao m_vao;
GLint m_loc_proj = -1;
GLint m_loc_view = -1;
GLint m_loc_model = -1;
GLsizei m_index_count = 0;
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_mesh() -> void;
auto build_shader() -> bool;
auto build_pipeline() -> bool;
auto build_post_pipeline() -> bool;
};
}