From 694f1b0ec3c119d4625dd1ab10145aed23642a6b Mon Sep 17 00:00:00 2001 From: Hunter Kvalevog Date: Thu, 13 Nov 2025 18:14:17 -0600 Subject: gl-vfog --- common/c_cpp/CMakeLists.txt | 31 +++++ gl-vfog/CMakeLists.txt | 9 ++ gl-vfog/fs.glsl | 60 ++++++++++ gl-vfog/geometry.hh | 108 +++++++++++++++++ gl-vfog/main.cc | 281 ++++++++++++++++++++++++++++++++++++++++++++ gl-vfog/vs.glsl | 12 ++ 6 files changed, 501 insertions(+) create mode 100644 gl-vfog/CMakeLists.txt create mode 100644 gl-vfog/fs.glsl create mode 100644 gl-vfog/geometry.hh create mode 100644 gl-vfog/main.cc create mode 100644 gl-vfog/vs.glsl diff --git a/common/c_cpp/CMakeLists.txt b/common/c_cpp/CMakeLists.txt index 6b4543f..f87b1d5 100644 --- a/common/c_cpp/CMakeLists.txt +++ b/common/c_cpp/CMakeLists.txt @@ -5,6 +5,9 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_library(common INTERFACE) +set(FETCHCONTENT_QUIET FALSE) +include(FetchContent) + # ----------------------------------------------------------------------------- # Language options # ----------------------------------------------------------------------------- @@ -26,6 +29,34 @@ if(NOT MINGW) target_link_options(common INTERFACE -fsanitize=address) endif() +# ----------------------------------------------------------------------------- +# Dependency: Dear ImGui +# ----------------------------------------------------------------------------- +if(DEMO_NEEDS_DEAR_IMGUI) + FetchContent_Declare( + DEAR_IMGUI + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG v1.92.4 + GIT_PROGRESS TRUE + GIT_SHALLOW TRUE + ) + FetchContent_MakeAvailable(DEAR_IMGUI) + message("dear_imgui_SOURCE_DIR: ${dear_imgui_SOURCE_DIR}") + add_library(common_dear_imgui STATIC + "${dear_imgui_SOURCE_DIR}/imgui.cpp" + "${dear_imgui_SOURCE_DIR}/imgui_demo.cpp" + "${dear_imgui_SOURCE_DIR}/imgui_draw.cpp" + "${dear_imgui_SOURCE_DIR}/imgui_tables.cpp" + "${dear_imgui_SOURCE_DIR}/imgui_widgets.cpp" + ) + target_include_directories(common_dear_imgui PUBLIC + "${dear_imgui_SOURCE_DIR}" + "${dear_imgui_SOURCE_DIR}/backends" + ) + target_compile_features(common_dear_imgui PRIVATE cxx_std_17) + target_link_libraries(common INTERFACE common_dear_imgui) +endif() + # ----------------------------------------------------------------------------- # Dependency: FFmpeg # ----------------------------------------------------------------------------- diff --git a/gl-vfog/CMakeLists.txt b/gl-vfog/CMakeLists.txt new file mode 100644 index 0000000..e085e04 --- /dev/null +++ b/gl-vfog/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.15 FATAL_ERROR) +project(gl-vfog CXX) + +set(DEMO_NEEDS_DEAR_IMGUI TRUE) +set(DEMO_NEEDS_SDL3 TRUE) +include("${CMAKE_CURRENT_LIST_DIR}/../common/c_cpp/CMakeLists.txt") + +add_executable(gl-vfog main.cc) +target_link_libraries(gl-vfog PRIVATE common) \ No newline at end of file diff --git a/gl-vfog/fs.glsl b/gl-vfog/fs.glsl new file mode 100644 index 0000000..00ec9b1 --- /dev/null +++ b/gl-vfog/fs.glsl @@ -0,0 +1,60 @@ +#version 330 core + +in vec3 f_p; + +out vec4 f_color; + +uniform vec3 u_cam; +uniform float u_time; +uniform int u_mode; +uniform float u_sigma; + +// Ray-AABB intersection +// ref: https://tavianator.com/2022/ray_box_boundary.html +bool intersect(vec3 ro, vec3 rd, vec3 bbmin, vec3 bbmax, out float t0, out float t1) +{ + vec3 inv = 1.0f / rd; + vec3 ta = (bbmin - ro) * inv; + vec3 tb = (bbmax - ro) * inv; + vec3 tmin = min(ta, tb); + vec3 tmax = max(ta, tb); + t0 = max(max(tmin.x, tmin.y), tmin.z); + t1 = min(min(tmax.x, tmax.y), tmax.z); + return t1 >= max(t0, 0.0f); +} + +// Mode 0: Basic +void main_0() +{ + vec3 bbmin = vec3(0.0f, 0.0f, 0.0f); + vec3 bbmax = vec3(1.0f, 1.0f, 1.0f); + + vec3 ro = u_cam; + vec3 rd = normalize(f_p - ro); + + float t0 = 0.0f; + float t1 = 0.0f; + if (!intersect(ro, rd, bbmin, bbmax, t0, t1)) { + f_color = vec4(0.0f, 0.0f, 0.0f, 0.0f); + return; + } + + // Clamp entry point in front of camera + float t_enter = max(t0, 0.0f); + float t_exit = t1; + float t_len = max(0.0f, t_exit - t_enter); + + // Beer-Lambert attenuation + // ref: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law + float a = 1.0f - exp(-u_sigma * t_len); + + f_color = vec4(vec3(1.0f), a); +} + +void main() +{ + switch (u_mode) { + case 0: { main_0(); } break; + default: { f_color = vec4(0.0f, 1.0f, 0.0f, 1.0f); } break; + } +} diff --git a/gl-vfog/geometry.hh b/gl-vfog/geometry.hh new file mode 100644 index 0000000..c48248a --- /dev/null +++ b/gl-vfog/geometry.hh @@ -0,0 +1,108 @@ +#ifndef _GEOMETRY_HH_ +#define _GEOMETRY_HH_ + +#include + +static inline float Radians(float degrees) +{ + return degrees * M_PI / 180.0f; +} + +class Vec3 +{ +public: + float x; + float y; + float z; +public: + Vec3(float x, float y, float z) + : x(x), y(y), z(z) { } + + inline Vec3 operator+(const Vec3& rhs) const { return Vec3(x + rhs.x, y + rhs.y, z + rhs.z); } + inline Vec3 operator-(const Vec3& rhs) const { return Vec3(x - rhs.x, y - rhs.y, z - rhs.z); } + + inline float* Base() { return &x; } + + inline float Length() { return std::sqrtf(x * x + y * y + z * z); } + + inline Vec3 Normalize() + { + float l = Length(); + if (l < 0.0f) { l = 1.0f; } + return Vec3(x / l, y / l, z / l); + } + + inline float Dot(const Vec3& rhs) + { + return x * rhs.x + y * rhs.y + z * rhs.z; + } + + inline Vec3 Cross(const Vec3& rhs) + { + return Vec3( + y * rhs.z - z * rhs.y, + z * rhs.x - x * rhs.z, + x * rhs.y - y * rhs.x + ); + } +}; + +class Mat4x4 +{ +private: + float m[4 * 4]; +public: + inline float& operator[](std::size_t s) { return m[s]; } + inline float operator[](std::size_t s) const { return m[s]; } + + inline float* Base() { return m; } + + inline Mat4x4 operator*(const Mat4x4& rhs) + { + Mat4x4 r = { }; + for (int col = 0; col < 4; ++col) { + for (int row = 0; row < 4; ++row) { + r[col * 4 + row] = + m[0 * 4 + row] * rhs[col * 4 + 0] + + m[1 * 4 + row] * rhs[col * 4 + 1] + + m[2 * 4 + row] * rhs[col * 4 + 2] + + m[3 * 4 + row] * rhs[col * 4 + 3]; + } + } + return r; + } +public: + static Mat4x4 LookAt(const Vec3& eye, const Vec3& at, const Vec3& up) + { + Vec3 f = (at - eye).Normalize(); + Vec3 s = f.Cross(up).Normalize(); + Vec3 u = s.Cross(f); + Vec3 t = Vec3(-s.Dot(eye), -u.Dot(eye), f.Dot(eye)); + + Mat4x4 m; + m[ 0] = s.x; m[ 1] = u.x; m[ 2] = -f.x; m[ 3] = 0.0f; + m[ 4] = s.y; m[ 5] = u.y; m[ 6] = -f.y; m[ 7] = 0.0f; + m[ 8] = s.z; m[ 9] = u.z; m[10] = -f.z; m[11] = 0.0f; + m[12] = t.x; m[13] = t.y; m[14] = t.z; m[15] = 1.0f; + + return m; + } + + static Mat4x4 Perspective(float fov_deg, float aspect, float z_near, float z_far) + { + const float fov_cot = 1.0f / std::tanf(Radians(fov_deg) / 2.0f); + + Mat4x4 m = { }; + + m[0*4+0] = fov_cot / aspect; + m[1*4+1] = fov_cot; + m[2*4+3] = -1.0f; + + m[2*4+2] = (z_far + z_near) / (z_near - z_far); + m[3*4+2] = (2.0f * z_near * z_far) / (z_near - z_far); + + return m; + } +}; + +#endif // _GEOMETRY_HH_ diff --git a/gl-vfog/main.cc b/gl-vfog/main.cc new file mode 100644 index 0000000..d14ff38 --- /dev/null +++ b/gl-vfog/main.cc @@ -0,0 +1,281 @@ +// OpenGL volumetric fog demo +// +// SPDX-License-Identifier: 0BSD +// +// Changelog: +// - 11/13/25: Initial release +// + +#define GLAD_GL_IMPLEMENTATION +#include "../common/c_cpp/thirdparty/glad33/glad33.h" + +#include +#include +#include + +#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM +#include "imgui.h" +#include "imgui_impl_opengl3.cpp" +#include "imgui_impl_sdl3.cpp" + +#include "geometry.hh" + +// Helper: Report errors via glGetError() after every OpenGL function call. +// macOS does not support glDebugMessageCallback +static void assert_gl_error(GLenum error, const char* expr, int line) +{ + if (error != GL_NO_ERROR) { + const char* error_string; + switch (error) { +#define BIND_ERROR(name) case name: { error_string = #name; }; break; + BIND_ERROR(GL_INVALID_ENUM); + BIND_ERROR(GL_INVALID_VALUE); + BIND_ERROR(GL_INVALID_OPERATION); + BIND_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION); + BIND_ERROR(GL_OUT_OF_MEMORY); +#undef BIND_ERROR + default: SDL_assert(0 && "Invalid GL_ERROR enum"); + } + SDL_Log("[Line #%u] %s caused %s\n", line, expr, error_string); + } +} +#define GL(expr) \ + expr; \ + for (GLenum _glcode; (_glcode = glGetError()) != GL_NO_ERROR; ) { \ + assert_gl_error(_glcode, #expr, __LINE__); \ + } + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; + + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { + fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError()); + return EXIT_FAILURE; + } + + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + + SDL_Window* wnd = SDL_CreateWindow("gl-vfog", 1024, 768, SDL_WINDOW_OPENGL); + if (!wnd) { + fprintf(stderr, "Failed to create window: %s\n", SDL_GetError()); + return EXIT_FAILURE; + } + + if (!SDL_GL_CreateContext(wnd)) { + fprintf(stderr, "Failed to create OpenGL context: %s\n", SDL_GetError()); + return EXIT_FAILURE; + } + SDL_GL_SetSwapInterval(1); + + if (!gladLoadGL(SDL_GL_GetProcAddress)) { + fprintf(stderr, "Failed to load OpenGL functions\n"); + return EXIT_FAILURE; + } + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui_ImplSDL3_Init(wnd, 0, 0); + ImGui_ImplOpenGL3_Init(); + ImGui::GetIO().IniFilename = NULL; + + // + // 0 1 + // +------------+ + // /| /| + // 4 / | 5 / | + // +------------+ | + // | | | | + // | | 3 | | 2 + // | +---------|--+ + // | / | / + // 7 |/ 6 |/ + // +------------+ + // + + const float vdata[] = { + 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + }; + + const uint16_t idata[] = { + 1, 0, 3, + 1, 3, 2, + 5, 1, 2, + 5, 2, 6, + 4, 5, 6, + 4, 6, 7, + 0, 4, 7, + 0, 7, 3, + 0, 1, 5, + 0, 5, 4, + 7, 6, 2, + 7, 2, 3, + }; + + GLuint vao; + GL(glGenVertexArrays(1, &vao)) + GL(glBindVertexArray(vao)); + + GLuint vbo; + GL(glGenBuffers(1, &vbo)); + GL(glBindBuffer(GL_ARRAY_BUFFER, vbo)); + GL(glBufferData(GL_ARRAY_BUFFER, sizeof(vdata), vdata, GL_STATIC_DRAW)); + + GLuint ibo; + GL(glGenBuffers(1, &ibo)); + GL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo)); + GL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idata), idata, GL_STATIC_DRAW)); + + GL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, NULL)); + GL(glEnableVertexAttribArray(0)); + + struct { + GLenum type; + const char* path; + } shaders[] = { + { .type = GL_VERTEX_SHADER, .path = "vs.glsl" }, + { .type = GL_FRAGMENT_SHADER, .path = "fs.glsl" }, + }; + + GLuint prog = GL(glCreateProgram()); + for (size_t i = 0; i < sizeof(shaders) / sizeof(*shaders); ++i) { + const char* src = (const char*)SDL_LoadFile(shaders[i].path, NULL); + if (!src) { + fprintf(stderr, "Failed to load shader %s: %s\n", shaders[i].path, SDL_GetError()); + return EXIT_FAILURE; + } + + GLuint shader = GL(glCreateShader(shaders[i].type)); + GL(glShaderSource(shader, 1, &src, NULL)); + GL(glCompileShader(shader)); + GLint compile_status = 0; + GL(glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status)); + if (compile_status != GL_TRUE) { + char error[1024] = { 0 }; + GL(glGetShaderInfoLog(shader, sizeof(error), NULL, error)); + SDL_Log("Error compiling shader %s: %s\n", shaders[i].path, error); + return EXIT_FAILURE; + } + GL(glAttachShader(prog, shader)); + } + GL(glLinkProgram(prog)); + + GLint prog_status; + GL(glGetProgramiv(prog, GL_LINK_STATUS, &prog_status)); + if (prog_status != GL_TRUE) { + char error[1024] = { 0 }; + GL(glGetProgramInfoLog(prog, sizeof(error), NULL, error)); + SDL_Log("Error linking program: %s\n", error); + return EXIT_FAILURE; + } + + GL(glUseProgram(prog)); + + GLuint u_view = GL(glGetUniformLocation(prog, "u_view")); + GLuint u_cam = GL(glGetUniformLocation(prog, "u_cam")); + GLuint u_time = GL(glGetUniformLocation(prog, "u_time")); + GLuint u_mode = GL(glGetUniformLocation(prog, "u_mode")); + GLuint u_sigma = GL(glGetUniformLocation(prog, "u_sigma")); + + bool running = true; + while (running) { + SDL_Event evt = { 0 }; + while (SDL_PollEvent(&evt)) { + ImGui_ImplSDL3_ProcessEvent(&evt); + switch (evt.type) { + case SDL_EVENT_QUIT: { + running = false; + } break; + } + } + + int wnd_x = 0; + int wnd_y = 0; + SDL_GetWindowSizeInPixels(wnd, &wnd_x, &wnd_y); + + ImGui_ImplSDL3_NewFrame(); + ImGui_ImplOpenGL3_NewFrame(); + ImGui::NewFrame(); + + // Controls + static int c_mode = 0; + static float c_sigma = 4.0f; + static float c_zoom = 1.5f; + ImGui::SetNextWindowPos(ImVec2(15, 15)); + ImGui::SetNextWindowSize(ImVec2(150, 768 - 30)); + if (ImGui::Begin("Controls", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) { + ImGui::SeparatorText("Controls"); + ImGui::DragFloat("Zoom", &c_zoom, 0.05f, 0.1f, 3.0f); + ImGui::SeparatorText("Settings"); + const char* modes[] = { "Basic" }; + if (ImGui::BeginCombo("Mode", modes[c_mode])) { + for (int i = 0; i < IM_ARRAYSIZE(modes); ++i) { + if (ImGui::Selectable(modes[c_mode], i == c_mode)) { + c_mode = i; + } + } + ImGui::EndCombo(); + } + switch (c_mode) { + case 0: { + ImGui::DragFloat("Sigma", &c_sigma, 0.05); + } break; + } + ImGui::End(); + } + + GL(glViewport(0, 0, wnd_x, wnd_y)); + GL(glEnable(GL_CULL_FACE)); + GL(glCullFace(GL_FRONT)); + GL(glEnable(GL_DEPTH_TEST)); + GL(glEnable(GL_BLEND)); + GL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); + GL(glClearColor(0.1f, 0.1f, 0.1f, 1.0f)); + GL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)); + + const float t = SDL_GetTicks() / 1e3f; + const float camt = t * 0.33f; + + Vec3 eye = Vec3(0.0f, 1.5f, 0.0f); + Vec3 tgt = Vec3(0.5f, 0.5f, 0.5f); + eye.x = tgt.x + SDL_sinf(camt) * c_zoom; + eye.z = tgt.z + SDL_cosf(camt) * c_zoom; + Vec3 up = Vec3(0.0f, 1.0f, 0.0f); + + Mat4x4 look = Mat4x4::LookAt(eye, tgt, up); + Mat4x4 proj = Mat4x4::Perspective(70.0f, (float)wnd_x / (float)wnd_y, 0.1f, 10.0f); + Mat4x4 view = proj * look; + + GL(glUniformMatrix4fv(u_view, 1, GL_FALSE, view.Base())) + GL(glUniform3fv(u_cam, 1, eye.Base())); + GL(glUniform1f(u_time, t)); + GL(glUniform1i(u_mode, c_mode)); + GL(glUniform1f(u_sigma, c_sigma)); + + GL(glDrawElements(GL_TRIANGLES, 3 * 12, GL_UNSIGNED_SHORT, NULL)); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + if (!SDL_GL_SwapWindow(wnd)) { + fprintf(stderr, "Failed to present window: %s", SDL_GetError()); + return EXIT_FAILURE; + } + } + + SDL_DestroyWindow(wnd); + + return EXIT_SUCCESS; +} diff --git a/gl-vfog/vs.glsl b/gl-vfog/vs.glsl new file mode 100644 index 0000000..bf8c8eb --- /dev/null +++ b/gl-vfog/vs.glsl @@ -0,0 +1,12 @@ +#version 330 core + +layout (location = 0) in vec3 v_p; + +out vec3 f_p; + +uniform mat4 u_view; + +void main() { + f_p = v_p; + gl_Position = u_view * vec4(v_p, 1.0f); +} \ No newline at end of file -- cgit v1.2.3