diff --git a/.gitignore b/.gitignore index d4edd11..42f519f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ imgui.ini # Compiled shaders *.spv + +# Screenshots +*.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 37a2f2a..a1ab0a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,10 +55,20 @@ 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 add_executable(cuber "cuber.cpp") target_include_directories(cuber PRIVATE ".") target_compile_features(cuber PRIVATE cxx_std_23) target_compile_options(cuber PRIVATE ${BASE_OPTIONS}) target_compile_definitions(cuber PRIVATE ${BASE_DEFINITIONS}) -target_link_libraries(cuber PRIVATE cbt_scene scenes_cube asio::asio ${BASE_LIBRARIES}) +target_link_libraries(cuber PRIVATE cbt_scene scenes_cube scenes_sphere asio::asio ${BASE_LIBRARIES}) diff --git a/cuber.cpp b/cuber.cpp index 3ad7ba1..933c52a 100644 --- a/cuber.cpp +++ b/cuber.cpp @@ -13,6 +13,7 @@ #include "cbt/window.hpp" #include "cbt/opengl/context.hpp" #include "scenes/cube.hpp" +#include "scenes/sphere.hpp" auto main(int argc, char const* argv[]) -> int { float max_duration_seconds = 0.0f; @@ -23,6 +24,7 @@ auto main(int argc, char const* argv[]) -> int { fmt::print("Usage: {} [--duration ] [--help|-h]\n", argv[0]); fmt::print(" --duration Auto-terminate after N seconds (for testing/CI)\n"); fmt::print(" S key Take screenshot (saved as screenshot.png)\n"); + fmt::print(" 1/2 key Switch between cube/sphere scene\n"); return 0; } if (arg == "--duration" && i + 1 < argc) { @@ -43,10 +45,14 @@ auto main(int argc, char const* argv[]) -> int { return 1; } - auto scn = cbt::scenes::cube(); - if (!scn.init()) { + auto cube_scn = cbt::scenes::cube(); + auto sphere_scn = cbt::scenes::sphere(); + + cbt::scene* active_scene = nullptr; + if (!cube_scn.init() || !sphere_scn.init()) { return 1; } + active_scene = &cube_scn; // signal handling + optional duration timer (via ASIO) asio::io_context io; @@ -84,13 +90,19 @@ auto main(int argc, char const* argv[]) -> int { if (glfwGetKey(win.raw(), GLFW_KEY_S) == GLFW_PRESS) { win.screenshot(); } + if (glfwGetKey(win.raw(), GLFW_KEY_1) == GLFW_PRESS) { + active_scene = &cube_scn; + } + if (glfwGetKey(win.raw(), GLFW_KEY_2) == GLFW_PRESS) { + active_scene = &sphere_scn; + } auto now = std::chrono::steady_clock::now(); auto dt = std::chrono::duration(now - prev).count(); prev = now; - scn.update(dt); - scn.render(); + active_scene->update(dt); + active_scene->render(); win.swap_buffers(); win.poll_events(); diff --git a/scenes/sphere.cpp b/scenes/sphere.cpp new file mode 100644 index 0000000..d8514ac --- /dev/null +++ b/scenes/sphere.cpp @@ -0,0 +1,195 @@ +#include +#include +#include + +#include "glad/glad.h" +#include "glm/gtc/matrix_transform.hpp" +#include "glm/gtc/type_ptr.hpp" + +#include "scenes/sphere.hpp" + +namespace cbt::scenes { + +sphere::sphere() { + m_start = std::chrono::steady_clock::now(); +} + +auto sphere::init() -> bool { + if (!build_shader()) { + return false; + } + build_mesh(); + glEnable(GL_DEPTH_TEST); + return true; +} + +auto sphere::update(float) -> void {} + +auto sphere::render() -> void { + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration(now - m_start).count(); + + glClearColor(0.15f, 0.15f, 0.2f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + auto aspect = 1280.0f / 720.0f; + auto proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f); + auto view = glm::translate(glm::mat4{1.0f}, glm::vec3{0.0f, 0.0f, -4.0f}); + auto model = glm::rotate(glm::mat4{1.0f}, elapsed, glm::vec3{1.0f, 0.3f, 0.2f}); + + m_prog.use(); + glUniformMatrix4fv(m_loc_proj, 1, GL_FALSE, glm::value_ptr(proj)); + glUniformMatrix4fv(m_loc_view, 1, GL_FALSE, glm::value_ptr(view)); + glUniformMatrix4fv(m_loc_model, 1, GL_FALSE, glm::value_ptr(model)); + + m_vao.bind(); + glDrawElements(GL_TRIANGLES, m_index_count, GL_UNSIGNED_INT, nullptr); + m_vao.unbind(); + m_prog.unuse(); +} + +auto sphere::build_mesh() -> void { + struct vertex { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 uv; + }; + + std::uint32_t const div = 32; + std::vector vertices; + std::vector indices; + + // Generate 6 cube faces, each with div x div vertices + auto add_face = [&](glm::vec3 const& center, glm::vec3 const& u_axis, + glm::vec3 const& v_axis) -> void { + for (std::uint32_t i = 0; i < div; ++i) { + for (std::uint32_t j = 0; j < div; ++j) { + float const s = float(i) / float(div - 1) * 2.0f - 1.0f; + float const t = float(j) / float(div - 1) * 2.0f - 1.0f; + + // Position on cube face + glm::vec3 pos = center + u_axis * s + v_axis * t; + + // FIX: normalize to project onto unit sphere + // (the original nrz.cpp used a broken formula with p=50.0) + float const len = glm::length(pos); + glm::vec3 normal = pos / len; + + vertices.push_back({normal, normal, {float(i) / float(div - 1), float(j) / float(div - 1)}}); + } + } + }; + + // +X face (right) + add_face(glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{0.0f, 0.0f, 1.0f}); + // -X face (left) + add_face(glm::vec3{-1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{0.0f, 0.0f, -1.0f}); + // +Y face (top) + add_face(glm::vec3{0.0f, 1.0f, 0.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 0.0f, -1.0f}); + // -Y face (bottom) + add_face(glm::vec3{0.0f, -1.0f, 0.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 0.0f, 1.0f}); + // +Z face (front) + add_face(glm::vec3{0.0f, 0.0f, 1.0f}, glm::vec3{1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}); + // -Z face (back) + add_face(glm::vec3{0.0f, 0.0f, -1.0f}, glm::vec3{-1.0f, 0.0f, 0.0f}, glm::vec3{0.0f, 1.0f, 0.0f}); + + // Generate indices for each face + std::uint32_t offset = 0; + for (std::uint32_t face = 0; face < 6; ++face) { + for (std::uint32_t i = 0; i < div - 1; ++i) { + for (std::uint32_t j = 0; j < div - 1; ++j) { + std::uint32_t const a = offset + i * div + j; + std::uint32_t const b = offset + (i + 1) * div + j; + std::uint32_t const c = offset + (i + 1) * div + j + 1; + std::uint32_t const d = offset + i * div + j + 1; + + // Two triangles per quad (consistent winding) + indices.push_back(a); + indices.push_back(b); + indices.push_back(d); + + indices.push_back(b); + indices.push_back(c); + indices.push_back(d); + } + } + offset += div * div; + } + + m_index_count = static_cast(indices.size()); + + m_vbo.upload(vertices.data(), vertices.size() * sizeof(vertex)); + m_ebo.upload(indices.data(), indices.size() * sizeof(std::uint32_t)); + + m_vao.bind(); + m_vbo.bind(); + m_ebo.bind(); + + // location 0: position (vec3) + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), nullptr); + + // location 1: normal (vec3) + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), + reinterpret_cast(offsetof(vertex, normal))); + + // location 2: uv (vec2) + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(vertex), + reinterpret_cast(3 * sizeof(float) + 3 * sizeof(float))); + + m_ebo.unbind(); + m_vbo.unbind(); + m_vao.unbind(); +} + +auto sphere::build_shader() -> bool { + char const* vert_src = R"glsl( + #version 410 core + layout(location = 0) in vec3 a_pos; + layout(location = 1) in vec3 a_normal; + layout(location = 2) in vec2 a_uv; + 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; + 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; + } + )glsl"; + + char const* frag_src = R"glsl( + #version 410 core + in vec3 v_normal; + in vec3 v_world_pos; + in vec2 v_uv; + 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 color = vec3(0.4, 0.6, 1.0); + vec3 result = (ambient + diff * 0.8) * color; + frag_color = vec4(result, 1.0); + } + )glsl"; + + if (!m_prog.compile_vertex(vert_src) || !m_prog.compile_fragment(frag_src) || !m_prog.link()) { + return false; + } + + m_loc_proj = glGetUniformLocation(m_prog.id(), "u_proj"); + m_loc_view = glGetUniformLocation(m_prog.id(), "u_view"); + m_loc_model = glGetUniformLocation(m_prog.id(), "u_model"); + + return true; +} + +} diff --git a/scenes/sphere.hpp b/scenes/sphere.hpp new file mode 100644 index 0000000..191c71a --- /dev/null +++ b/scenes/sphere.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "glm/glm.hpp" + +#include "cbt/scene.hpp" +#include "cbt/opengl/buffer.hpp" +#include "cbt/opengl/shader.hpp" +#include "cbt/opengl/vao.hpp" + +namespace cbt::scenes { + +class sphere final : public scene { +public: + sphere(); + auto init() -> bool override; + auto update(float delta_time) -> void override; + auto render() -> void override; + +private: + opengl::shader m_prog; + opengl::buffer m_vbo; + opengl::buffer m_ebo{opengl::buffer_type::index}; + opengl::vao m_vao; + + GLint m_loc_proj = -1; + GLint m_loc_view = -1; + GLint m_loc_model = -1; + + GLsizei m_index_count = 0; + + std::chrono::steady_clock::time_point m_start; + + auto build_mesh() -> void; + auto build_shader() -> bool; +}; + +}