summaryrefslogtreecommitdiff
path: root/gl-vfog
diff options
context:
space:
mode:
authorHunter Kvalevog <hunter@kvog.sh>2025-11-13 18:14:17 -0600
committerHunter Kvalevog <hunter@kvog.sh>2025-11-13 18:14:17 -0600
commit694f1b0ec3c119d4625dd1ab10145aed23642a6b (patch)
treec3d7a6ce9fd39135b51df1d6fad9130495b550a8 /gl-vfog
parent012d4ccc9f34ac50b4c68e5c97e36d9d9813fc42 (diff)
gl-vfog
Diffstat (limited to 'gl-vfog')
-rw-r--r--gl-vfog/CMakeLists.txt9
-rw-r--r--gl-vfog/fs.glsl60
-rw-r--r--gl-vfog/geometry.hh108
-rw-r--r--gl-vfog/main.cc281
-rw-r--r--gl-vfog/vs.glsl12
5 files changed, 470 insertions, 0 deletions
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 <cmath>
+
+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 <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL_opengl.h>
+
+#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