├── black_hole.exe ├── Gravity_Sim.zip ├── vcpkg.json ├── grid.frag ├── grid.vert ├── vs_code ├── c_cpp_properties.json ├── launch.json ├── tasks.json └── settings.json ├── CMakeLists.txt ├── .gitignore ├── README.md ├── geodesic.comp ├── 2D_lensing.cpp ├── ray_tracing.cpp ├── CPU-geodesic.cpp └── black_hole.cpp /black_hole.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kavan010/black_hole/HEAD/black_hole.exe -------------------------------------------------------------------------------- /Gravity_Sim.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kavan010/black_hole/HEAD/Gravity_Sim.zip -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "glfw3", 4 | "glm", 5 | "glew" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /grid.frag: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | void main() { 4 | FragColor = vec4(0.5, 0.5, 0.5, 0.7); // translucent blue lines 5 | } 6 | -------------------------------------------------------------------------------- /grid.vert: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | layout(location = 0) in vec3 aPos; 3 | uniform mat4 viewProj; 4 | void main() { 5 | gl_Position = viewProj * vec4(aPos, 1.0); 6 | } 7 | -------------------------------------------------------------------------------- /vs_code/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "C:/msys64/mingw64/include", 8 | "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.8/include" 9 | ], 10 | "defines": [], 11 | "compilerPath": "C:/msys64/mingw64/bin/g++.exe", 12 | "cStandard": "c17", 13 | "cppStandard": "c++17", 14 | "intelliSenseMode": "windows-gcc-x64" 15 | } 16 | ], 17 | "version": 4 18 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(BlackHoleSim) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | # Dependencies 8 | find_package(GLEW REQUIRED) 9 | find_package(glfw3 CONFIG REQUIRED) 10 | find_package(glm CONFIG REQUIRED) 11 | find_package(OpenGL REQUIRED) 12 | 13 | # Common dependencies 14 | set(DEPS glfw GLEW::GLEW glm::glm OpenGL::GL) 15 | 16 | # 2D Lensing executable 17 | add_executable(BlackHole2D 2D_lensing.cpp) 18 | target_link_libraries(BlackHole2D PRIVATE ${DEPS}) 19 | target_include_directories(BlackHole2D PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 20 | 21 | # 3D Black Hole executable 22 | add_executable(BlackHole3D black_hole.cpp) 23 | target_link_libraries(BlackHole3D PRIVATE ${DEPS}) 24 | target_include_directories(BlackHole3D PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 25 | 26 | # Shader files (copy to output dir) 27 | file(GLOB SHADERS 28 | "${CMAKE_CURRENT_SOURCE_DIR}/*.vert" 29 | "${CMAKE_CURRENT_SOURCE_DIR}/*.frag" 30 | "${CMAKE_CURRENT_SOURCE_DIR}/*.comp" 31 | ) 32 | 33 | foreach(shader ${SHADERS}) 34 | add_custom_command(TARGET BlackHole3D POST_BUILD 35 | COMMAND ${CMAKE_COMMAND} -E copy_if_different 36 | ${shader} 37 | $ 38 | ) 39 | endforeach() 40 | 41 | -------------------------------------------------------------------------------- /vs_code/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "g++ Debug", 6 | "type": "cppdbg", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/bin/black_hole.exe", 9 | "args": [], 10 | "stopAtEntry": false, 11 | "cwd": "${workspaceFolder}/src", 12 | "environment": [], 13 | "externalConsole": false, 14 | "MIMode": "gdb", 15 | "miDebuggerPath": "C:/msys64/mingw64/bin/gdb.exe", 16 | "setupCommands": [ 17 | { 18 | "description": "Enable pretty-printing for gdb", 19 | "text": "-enable-pretty-printing", 20 | "ignoreFailures": true 21 | } 22 | ], 23 | "preLaunchTask": "build" 24 | }, 25 | { 26 | "name": "CUDA Debug", 27 | "type": "cppdbg", 28 | "request": "launch", 29 | "program": "${workspaceFolder}/bin/black_hole.exe", 30 | "args": [], 31 | "stopAtEntry": false, 32 | "cwd": "${workspaceFolder}/src", 33 | "environment": [], 34 | "externalConsole": false, 35 | "MIMode": "gdb", 36 | "miDebuggerPath": "C:/msys64/mingw64/bin/gdb.exe", 37 | "setupCommands": [ 38 | { 39 | "description": "Enable pretty-printing for gdb", 40 | "text": "-enable-pretty-printing", 41 | "ignoreFailures": true 42 | } 43 | ], 44 | "preLaunchTask": "build-cuda" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /vs_code/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "cppbuild", 7 | "command": "C:/msys64/mingw64/bin/g++.exe", 8 | "args": [ 9 | "-fdiagnostics-color=always", 10 | "-g", 11 | "${workspaceFolder}/src/black_hole.cpp", 12 | "-o", 13 | "${workspaceFolder}/bin/black_hole.exe", 14 | "-IC:/msys64/mingw64/include", 15 | "-LC:/msys64/mingw64/lib", 16 | "-lglfw3", 17 | "-lglew32", 18 | "-lopengl32", 19 | "-lgdi32" 20 | ], 21 | "options": { 22 | "cwd": "${workspaceFolder}" 23 | }, 24 | "problemMatcher": [ 25 | "$gcc" 26 | ], 27 | "group": { 28 | "kind": "build", 29 | "isDefault": true 30 | }, 31 | "detail": "compiler: C:/msys64/mingw64/bin/g++.exe" 32 | }, 33 | { 34 | "label": "build-cuda", 35 | "type": "shell", 36 | "command": "\"C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.8/bin/nvcc.exe\"", 37 | "args": [ 38 | "-o", 39 | "${workspaceFolder}/bin/black_hole_cuda.exe", 40 | "${workspaceFolder}/src/black_hole.cu", 41 | "-IC:/msys64/mingw64/include", 42 | "-IC:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.8/include", 43 | "-LC:/msys64/mingw64/lib", 44 | "-LC:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.8/lib/x64", 45 | "-lglfw3", 46 | "-lglew32", 47 | "-lopengl32", 48 | "-lgdi32", 49 | "-lcudart" 50 | ], 51 | "options": { 52 | "cwd": "${workspaceFolder}" 53 | }, 54 | "group": "build" 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ##### Windows 2 | # Windows thumbnail cache files 3 | Thumbs.db 4 | Thumbs.db:encryptable 5 | ehthumbs.db 6 | ehthumbs_vista.db 7 | 8 | # Dump file 9 | *.stackdump 10 | 11 | # Folder config file 12 | [Dd]esktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msix 21 | *.msm 22 | *.msp 23 | 24 | # Windows shortcuts 25 | *.lnk 26 | 27 | ##### MacOS 28 | # General 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | ##### Vim 34 | # Swap 35 | [._]*.s[a-v][a-z] 36 | !*.svg # comment out if you don't need vector files 37 | [._]*.sw[a-p] 38 | [._]s[a-rt-v][a-z] 39 | [._]ss[a-gi-z] 40 | [._]sw[a-p] 41 | 42 | ##### VisualStudioCode 43 | .vscode/* 44 | !.vscode/settings.json 45 | !.vscode/tasks.json 46 | !.vscode/launch.json 47 | !.vscode/extensions.json 48 | *.code-workspace 49 | 50 | # CMake 51 | cmake-build-*/ 52 | 53 | build 54 | 55 | ##### CMake 56 | CMakeLists.txt.user 57 | CMakeCache.txt 58 | CMakeFiles 59 | CMakeScripts 60 | Testing 61 | Makefile 62 | cmake_install.cmake 63 | install_manifest.txt 64 | compile_commands.json 65 | CTestTestfile.cmake 66 | _deps 67 | 68 | ##### C++ 69 | # Prerequisites 70 | *.d 71 | 72 | # Compiled Object files 73 | *.slo 74 | *.lo 75 | *.o 76 | *.obj 77 | 78 | # Precompiled Headers 79 | *.gch 80 | *.pch 81 | 82 | # Compiled Dynamic libraries 83 | *.so 84 | *.dylib 85 | *.dll 86 | 87 | # Compiled Static libraries 88 | *.lai 89 | *.la 90 | *.a 91 | *.lib 92 | 93 | # Executables 94 | *.exe 95 | *.out 96 | *.app 97 | 98 | # C/C++ binary extension file 99 | *.bin 100 | 101 | ##### C 102 | # Prerequisites 103 | *.d 104 | 105 | # Object files 106 | *.o 107 | *.ko 108 | *.obj 109 | *.elf 110 | 111 | # Linker output 112 | *.ilk 113 | *.map 114 | *.exp 115 | 116 | # Precompiled Headers 117 | *.gch 118 | *.pch 119 | 120 | # Libraries 121 | *.lib 122 | *.a 123 | *.la 124 | *.lo 125 | 126 | # Shared objects (inc. Windows DLLs) 127 | *.dll 128 | *.so 129 | *.so.* 130 | *.dylib 131 | 132 | # Executables 133 | *.exe 134 | *.out 135 | *.app 136 | *.i*86 137 | *.x86_64 138 | *.hex 139 | 140 | # Debug files 141 | *.dSYM/ 142 | *.su 143 | *.idb 144 | *.pdb 145 | 146 | # Vcpkg 147 | vcpkg_installed 148 | -------------------------------------------------------------------------------- /vs_code/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "ostream": "cpp", 4 | "array": "cpp", 5 | "atomic": "cpp", 6 | "bit": "cpp", 7 | "*.tcc": "cpp", 8 | "cctype": "cpp", 9 | "charconv": "cpp", 10 | "clocale": "cpp", 11 | "cmath": "cpp", 12 | "compare": "cpp", 13 | "concepts": "cpp", 14 | "cstdarg": "cpp", 15 | "cstddef": "cpp", 16 | "cstdint": "cpp", 17 | "cstdio": "cpp", 18 | "cstdlib": "cpp", 19 | "ctime": "cpp", 20 | "cwchar": "cpp", 21 | "cwctype": "cpp", 22 | "deque": "cpp", 23 | "string": "cpp", 24 | "unordered_map": "cpp", 25 | "vector": "cpp", 26 | "exception": "cpp", 27 | "algorithm": "cpp", 28 | "functional": "cpp", 29 | "iterator": "cpp", 30 | "memory": "cpp", 31 | "memory_resource": "cpp", 32 | "numeric": "cpp", 33 | "optional": "cpp", 34 | "random": "cpp", 35 | "string_view": "cpp", 36 | "system_error": "cpp", 37 | "tuple": "cpp", 38 | "type_traits": "cpp", 39 | "utility": "cpp", 40 | "format": "cpp", 41 | "initializer_list": "cpp", 42 | "iosfwd": "cpp", 43 | "iostream": "cpp", 44 | "istream": "cpp", 45 | "limits": "cpp", 46 | "new": "cpp", 47 | "numbers": "cpp", 48 | "span": "cpp", 49 | "stdexcept": "cpp", 50 | "streambuf": "cpp", 51 | "text_encoding": "cpp", 52 | "cinttypes": "cpp", 53 | "typeinfo": "cpp", 54 | "variant": "cpp", 55 | "chrono": "cpp", 56 | "ratio": "cpp", 57 | "iomanip": "cpp", 58 | "semaphore": "cpp", 59 | "sstream": "cpp", 60 | "stop_token": "cpp", 61 | "thread": "cpp", 62 | "cstring": "cpp", 63 | "ios": "cpp", 64 | "list": "cpp", 65 | "xfacet": "cpp", 66 | "xhash": "cpp", 67 | "xiosbase": "cpp", 68 | "xlocale": "cpp", 69 | "xlocinfo": "cpp", 70 | "xlocnum": "cpp", 71 | "xmemory": "cpp", 72 | "xstddef": "cpp", 73 | "xstring": "cpp", 74 | "xtr1common": "cpp", 75 | "xutility": "cpp" 76 | } 77 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **black**_**hole** 2 | 3 | Black hole simulation project 4 | 5 | Here is the black hole raw code, everything will be inside a src bin incase you want to copy the files 6 | 7 | I'm writing this as I'm beginning this project (hopefully I complete it ;D) here is what I plan to do: 8 | 9 | 1. Ray-tracing : add ray tracing to the gravity simulation to simulate gravitational lensing 10 | 11 | 2. Accretion disk : simulate accreciate disk using the ray tracing + the halos 12 | 13 | 3. Spacetime curvature : demonstrate visually the "trapdoor in spacetime" that is black holes using spacetime grid 14 | 15 | 4. [optional] try to make it run realtime ;D 16 | 17 | I hope it works :/ 18 | 19 | Edit: After completion of project - 20 | 21 | Thank you everyone for checking out the video, if you haven't it explains code in detail: https://www.youtube.com/watch?v=8-B6ryuBkCM 22 | 23 | ## **Building Requirements:** 24 | 25 | 1. C++ Compiler supporting C++ 17 or newer 26 | 27 | 2. [Cmake](https://cmake.org/) 28 | 29 | 3. [Vcpkg](https://vcpkg.io/en/) 30 | 31 | 4. [Git](https://git-scm.com/) 32 | 33 | ## **Build Instructions:** 34 | 35 | 1. Clone the repository: 36 | - `git clone https://github.com/kavan010/black_hole.git` 37 | 2. CD into the newly cloned directory 38 | - `cd ./black_hole` 39 | 3. Install dependencies with Vcpkg 40 | - `vcpkg install` 41 | 4. Get the vcpkg cmake toolchain file path 42 | - `vcpkg integrate install` 43 | - This will output something like : `CMake projects should use: "-DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake"` 44 | 5. Create a build directory 45 | - `mkdir build` 46 | 6. Configure project with CMake 47 | - `cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake` 48 | - Use the vcpkg cmake toolchain path from above 49 | 7. Build the project 50 | - `cmake --build build` 51 | 8. Run the program 52 | - The executables will be located in the build folder 53 | 54 | ### Alternative: Debian/Ubuntu apt workaround 55 | 56 | If you don't want to use vcpkg, or you just need a quick way to install the native development packages on Debian/Ubuntu, install these packages and then run the normal CMake steps above: 57 | 58 | ```bash 59 | sudo apt update 60 | sudo apt install build-essential cmake \ 61 | libglew-dev libglfw3-dev libglm-dev libgl1-mesa-dev 62 | ``` 63 | 64 | This provides the GLEW, GLFW, GLM and OpenGL development files so `find_package(...)` calls in `CMakeLists.txt` can locate the libraries. After installing, run the `cmake -B build -S .` and `cmake --build build` commands as shown in the Build Instructions. 65 | 66 | ## **How the code works:** 67 | for 2D: simple, just run 2D_lensing.cpp with the nessesary dependencies installed. 68 | 69 | for 3D: black_hole.cpp and geodesic.comp work together to run the simuation faster using GPU, essentially it sends over a UBO and geodesic.comp runs heavy calculations using that data. 70 | 71 | should work with nessesary dependencies installed, however I have only run it on windows with my GPU so am not sure! 72 | 73 | LMK if you would like an in-depth explanation of how the code works aswell :) 74 | -------------------------------------------------------------------------------- /geodesic.comp: -------------------------------------------------------------------------------- 1 | #version 430 2 | layout(local_size_x = 16, local_size_y = 16) in; 3 | 4 | layout(binding = 0, rgba8) writeonly uniform image2D outImage; 5 | layout(std140, binding = 1) uniform Camera { 6 | vec3 camPos; float _pad0; 7 | vec3 camRight; float _pad1; 8 | vec3 camUp; float _pad2; 9 | vec3 camForward; float _pad3; 10 | float tanHalfFov; 11 | float aspect; 12 | bool moving; 13 | int _pad4; 14 | } cam; 15 | 16 | layout(std140, binding = 2) uniform Disk { 17 | float disk_r1; 18 | float disk_r2; 19 | float disk_num; 20 | float thickness; 21 | }; 22 | 23 | layout(std140, binding = 3) uniform Objects { 24 | int numObjects; 25 | vec4 objPosRadius[16]; 26 | vec4 objColor[16]; 27 | float mass[16]; 28 | }; 29 | 30 | const float SagA_rs = 1.269e10; 31 | const float D_LAMBDA = 1e7; 32 | const double ESCAPE_R = 1e30; 33 | 34 | // Globals to store hit info 35 | vec4 objectColor = vec4(0.0); 36 | vec3 hitCenter = vec3(0.0); 37 | float hitRadius = 0.0; 38 | 39 | struct Ray { 40 | float x, y, z, r, theta, phi; 41 | float dr, dtheta, dphi; 42 | float E, L; 43 | }; 44 | Ray initRay(vec3 pos, vec3 dir) { 45 | Ray ray; 46 | ray.x = pos.x; ray.y = pos.y; ray.z = pos.z; 47 | ray.r = length(pos); 48 | ray.theta = acos(pos.z / ray.r); 49 | ray.phi = atan(pos.y, pos.x); 50 | 51 | float dx = dir.x, dy = dir.y, dz = dir.z; 52 | ray.dr = sin(ray.theta)*cos(ray.phi)*dx + sin(ray.theta)*sin(ray.phi)*dy + cos(ray.theta)*dz; 53 | ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + cos(ray.theta)*sin(ray.phi)*dy - sin(ray.theta)*dz) / ray.r; 54 | ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / (ray.r * sin(ray.theta)); 55 | 56 | ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi; 57 | float f = 1.0 - SagA_rs / ray.r; 58 | float dt_dL = sqrt((ray.dr*ray.dr)/f + ray.r*ray.r*(ray.dtheta*ray.dtheta + sin(ray.theta)*sin(ray.theta)*ray.dphi*ray.dphi)); 59 | ray.E = f * dt_dL; 60 | 61 | return ray; 62 | } 63 | 64 | bool intercept(Ray ray, float rs) { 65 | return ray.r <= rs; 66 | } 67 | // Returns true on hit, captures center, radius, and base color 68 | bool interceptObject(Ray ray) { 69 | vec3 P = vec3(ray.x, ray.y, ray.z); 70 | for (int i = 0; i < numObjects; ++i) { 71 | vec3 center = objPosRadius[i].xyz; 72 | float radius = objPosRadius[i].w; 73 | if (distance(P, center) <= radius) { 74 | objectColor = objColor[i]; 75 | hitCenter = center; 76 | hitRadius = radius; 77 | return true; 78 | } 79 | } 80 | return false; 81 | } 82 | 83 | void geodesicRHS(Ray ray, out vec3 d1, out vec3 d2) { 84 | float r = ray.r, theta = ray.theta; 85 | float dr = ray.dr, dtheta = ray.dtheta, dphi = ray.dphi; 86 | float f = 1.0 - SagA_rs / r; 87 | float dt_dL = ray.E / f; 88 | 89 | d1 = vec3(dr, dtheta, dphi); 90 | d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL 91 | + (SagA_rs / (2.0 * r*r * f)) * dr * dr 92 | + r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi); 93 | d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi; 94 | d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi; 95 | } 96 | void rk4Step(inout Ray ray, float dL) { 97 | vec3 k1a, k1b; 98 | geodesicRHS(ray, k1a, k1b); 99 | 100 | ray.r += dL * k1a.x; 101 | ray.theta += dL * k1a.y; 102 | ray.phi += dL * k1a.z; 103 | ray.dr += dL * k1b.x; 104 | ray.dtheta += dL * k1b.y; 105 | ray.dphi += dL * k1b.z; 106 | 107 | ray.x = ray.r * sin(ray.theta) * cos(ray.phi); 108 | ray.y = ray.r * sin(ray.theta) * sin(ray.phi); 109 | ray.z = ray.r * cos(ray.theta); 110 | } 111 | bool crossesEquatorialPlane(vec3 oldPos, vec3 newPos) { 112 | bool crossed = (oldPos.y * newPos.y < 0.0); 113 | float r = length(vec2(newPos.x, newPos.z)); 114 | return crossed && (r >= disk_r1 && r <= disk_r2); 115 | } 116 | 117 | void main() { 118 | int WIDTH = cam.moving ? 200 : 200; 119 | int HEIGHT = cam.moving ? 150 : 150; 120 | 121 | ivec2 pix = ivec2(gl_GlobalInvocationID.xy); 122 | if (pix.x >= WIDTH || pix.y >= HEIGHT) return; 123 | 124 | // Init Ray 125 | float u = (2.0 * (pix.x + 0.5) / WIDTH - 1.0) * cam.aspect * cam.tanHalfFov; 126 | float v = (1.0 - 2.0 * (pix.y + 0.5) / HEIGHT) * cam.tanHalfFov; 127 | vec3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward); 128 | Ray ray = initRay(cam.camPos, dir); 129 | 130 | vec4 color = vec4(0.0); 131 | vec3 prevPos = vec3(ray.x, ray.y, ray.z); 132 | float lambda = 0.0; 133 | 134 | bool hitBlackHole = false; 135 | bool hitDisk = false; 136 | bool hitObject = false; 137 | 138 | int steps = cam.moving ? 60000 : 60000; 139 | 140 | for (int i = 0; i < steps; ++i) { 141 | if (intercept(ray, SagA_rs)) { hitBlackHole = true; break; } 142 | rk4Step(ray, D_LAMBDA); 143 | lambda += D_LAMBDA; 144 | 145 | vec3 newPos = vec3(ray.x, ray.y, ray.z); 146 | if (crossesEquatorialPlane(prevPos, newPos)) { hitDisk = true; break; } 147 | if (interceptObject(ray)) { hitObject = true; break; } 148 | prevPos = newPos; 149 | if (ray.r > ESCAPE_R) break; 150 | } 151 | 152 | if (hitDisk) { 153 | double r = length(vec3(ray.x, ray.y, ray.z)) / disk_r2; 154 | vec3 diskColor = vec3(1.0, r, 0.2); 155 | //r = 1.0 - abs(r - 0.5) * 2.0; 156 | color = vec4(diskColor, r); 157 | 158 | } else if (hitBlackHole) { 159 | color = vec4(0.0, 0.0, 0.0, 1.0); 160 | 161 | } else if (hitObject) { 162 | // Compute shading 163 | vec3 P = vec3(ray.x, ray.y, ray.z); 164 | vec3 N = normalize(P - hitCenter); 165 | vec3 V = normalize(cam.camPos - P); 166 | float ambient = 0.1; 167 | float diff = max(dot(N, V), 0.0); 168 | float intensity = ambient + (1.0 - ambient) * diff; 169 | vec3 shaded = objectColor.rgb * intensity; 170 | color = vec4(shaded, objectColor.a); 171 | 172 | } else { 173 | color = vec4(0.0); 174 | } 175 | 176 | imageStore(outImage, pix, color); 177 | } 178 | -------------------------------------------------------------------------------- /2D_lensing.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define _USE_MATH_DEFINES 9 | #include 10 | #ifndef M_PI 11 | #define M_PI 3.14159265358979323846 12 | #endif 13 | using namespace glm; 14 | using namespace std; 15 | 16 | double c = 299792458.0; 17 | double G = 6.67430e-11; 18 | 19 | struct Ray; 20 | void rk4Step(Ray& ray, double dλ, double rs); 21 | 22 | // --- Structs --- // 23 | struct Engine { 24 | GLFWwindow* window; 25 | int WIDTH = 800; 26 | int HEIGHT = 600; 27 | float width = 100000000000.0f; // Width of the viewport in meters 28 | float height = 75000000000.0f; // Height of the viewport in meters 29 | 30 | // Navigation state 31 | float offsetX = 0.0f, offsetY = 0.0f; 32 | float zoom = 1.0f; 33 | bool middleMousePressed = false; 34 | double lastMouseX = 0.0, lastMouseY = 0; 35 | 36 | Engine() { 37 | if (!glfwInit()) { 38 | cerr << "Failed to initialize GLFW" << endl; 39 | exit(EXIT_FAILURE); 40 | } 41 | window = glfwCreateWindow(WIDTH, HEIGHT, "Black Hole Simulation", NULL, NULL); 42 | if (!window) { 43 | cerr << "Failed to create GLFW window" << endl; 44 | glfwTerminate(); 45 | exit(EXIT_FAILURE); 46 | } 47 | glfwMakeContextCurrent(window); 48 | glewExperimental = GL_TRUE; 49 | if (glewInit() != GLEW_OK) { 50 | cerr << "Failed to initialize GLEW" << endl; 51 | glfwDestroyWindow(window); 52 | glfwTerminate(); 53 | exit(EXIT_FAILURE); 54 | } 55 | glViewport(0, 0, WIDTH, HEIGHT);; 56 | } 57 | 58 | void run() { 59 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 60 | glMatrixMode(GL_PROJECTION); 61 | glLoadIdentity(); 62 | double left = -width + offsetX; 63 | double right = width + offsetX; 64 | double bottom = -height + offsetY; 65 | double top = height + offsetY; 66 | glOrtho(left, right, bottom, top, -1.0, 1.0); 67 | glMatrixMode(GL_MODELVIEW); 68 | glLoadIdentity(); 69 | } 70 | }; 71 | Engine engine; 72 | struct BlackHole { 73 | vec3 position; 74 | double mass; 75 | double radius; 76 | double r_s; 77 | 78 | BlackHole(vec3 pos, float m) : position(pos), mass(m) {r_s = 2.0 * G * mass / (c*c);} 79 | void draw() { 80 | glBegin(GL_TRIANGLE_FAN); 81 | glColor3f(1.0f, 0.0f, 0.0f); // Red color for the black hole 82 | glVertex2f(0.0f, 0.0f); // Center 83 | for(int i = 0; i <= 100; i++) { 84 | float angle = 2.0f * M_PI * i / 100; 85 | float x = r_s * cos(angle); // Radius of 0.1 86 | float y = r_s * sin(angle); 87 | glVertex2f(x, y); 88 | } 89 | glEnd(); 90 | } 91 | }; 92 | BlackHole SagA(vec3(0.0f, 0.0f, 0.0f), 8.54e36); // Sagittarius A black hole 93 | struct Ray{ 94 | // -- cartesian coords -- // 95 | double x; double y; 96 | // -- polar coords -- // 97 | double r; double phi; 98 | double dr; double dphi; 99 | vector trail; // trail of points 100 | double E, L; // conserved quantities 101 | 102 | Ray(vec2 pos, vec2 dir) : x(pos.x), y(pos.y), r(sqrt(pos.x * pos.x + pos.y * pos.y)), phi(atan2(pos.y, pos.x)), dr(dir.x), dphi(dir.y) { 103 | // step 1) get polar coords (r, phi) : 104 | this->r = sqrt(x*x + y*y); 105 | this->phi = atan2(y, x); 106 | // step 2) seed velocities : 107 | dr = dir.x * cos(phi) + dir.y * sin(phi); // m/s 108 | dphi = ( -dir.x * sin(phi) + dir.y * cos(phi) ) / r; 109 | // step 3) store conserved quantities 110 | L = r*r * dphi; 111 | double f = 1.0 - SagA.r_s/r; 112 | double dt_dλ = sqrt( (dr*dr)/(f*f) + (r*r*dphi*dphi)/f ); 113 | E = f * dt_dλ; 114 | // step 4) start trail : 115 | trail.push_back({x, y}); 116 | } 117 | void draw(const std::vector& rays) { 118 | // draw current ray positions as points 119 | glPointSize(2.0f); 120 | glColor3f(1.0f, 0.0f, 0.0f); 121 | glBegin(GL_POINTS); 122 | for (const auto& ray : rays) { 123 | glVertex2f(ray.x, ray.y); 124 | } 125 | glEnd(); 126 | 127 | // turn on blending for the trails 128 | glEnable(GL_BLEND); 129 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 130 | glLineWidth(1.0f); 131 | 132 | // draw each trail with fading alpha 133 | for (const auto& ray : rays) { 134 | size_t N = ray.trail.size(); 135 | if (N < 2) continue; 136 | 137 | glBegin(GL_LINE_STRIP); 138 | for (size_t i = 0; i < N; ++i) { 139 | // older points (i=0) get alpha≈0, newer get alpha≈1 140 | float alpha = float(i) / float(N - 1); 141 | glColor4f(1.0f, 1.0f, 1.0f, std::max(alpha, 0.05f)); 142 | glVertex2f(ray.trail[i].x, ray.trail[i].y); 143 | } 144 | glEnd(); 145 | } 146 | 147 | glDisable(GL_BLEND); 148 | } 149 | void step(double dλ, double rs) { 150 | // 1) integrate (r,φ,dr,dφ) 151 | if(r <= rs) return; // stop if inside the event horizon 152 | rk4Step(*this, dλ, rs); 153 | 154 | // 2) convert back to cartesian x,y 155 | x = r * cos(phi); 156 | y = r * sin(phi); 157 | 158 | // 3) record the trail 159 | trail.push_back({ float(x), float(y) }); 160 | } 161 | }; 162 | vector rays; 163 | 164 | void geodesicRHS(const Ray& ray, double rhs[4], double rs) { 165 | double r = ray.r; 166 | double dr = ray.dr; 167 | double dphi = ray.dphi; 168 | double E = ray.E; 169 | 170 | double f = 1.0 - rs/r; 171 | 172 | // dr/dλ = dr 173 | rhs[0] = dr; 174 | // dφ/dλ = dphi 175 | rhs[1] = dphi; 176 | 177 | // d²r/dλ² from Schwarzschild null geodesic: 178 | double dt_dλ = E / f; 179 | rhs[2] = 180 | - (rs/(2*r*r)) * f * (dt_dλ*dt_dλ) 181 | + (rs/(2*r*r*f)) * (dr*dr) 182 | + (r - rs) * (dphi*dphi); 183 | 184 | // d²φ/dλ² = -2*(dr * dphi) / r 185 | rhs[3] = -2.0 * dr * dphi / r; 186 | } 187 | void addState(const double a[4], const double b[4], double factor, double out[4]) { 188 | for (int i = 0; i < 4; i++) 189 | out[i] = a[i] + b[i] * factor; 190 | } 191 | void rk4Step(Ray& ray, double dλ, double rs) { 192 | double y0[4] = { ray.r, ray.phi, ray.dr, ray.dphi }; 193 | double k1[4], k2[4], k3[4], k4[4], temp[4]; 194 | 195 | geodesicRHS(ray, k1, rs); 196 | addState(y0, k1, dλ/2.0, temp); 197 | Ray r2 = ray; r2.r=temp[0]; r2.phi=temp[1]; r2.dr=temp[2]; r2.dphi=temp[3]; 198 | geodesicRHS(r2, k2, rs); 199 | 200 | addState(y0, k2, dλ/2.0, temp); 201 | Ray r3 = ray; r3.r=temp[0]; r3.phi=temp[1]; r3.dr=temp[2]; r3.dphi=temp[3]; 202 | geodesicRHS(r3, k3, rs); 203 | 204 | addState(y0, k3, dλ, temp); 205 | Ray r4 = ray; r4.r=temp[0]; r4.phi=temp[1]; r4.dr=temp[2]; r4.dphi=temp[3]; 206 | geodesicRHS(r4, k4, rs); 207 | 208 | ray.r += (dλ/6.0)*(k1[0] + 2*k2[0] + 2*k3[0] + k4[0]); 209 | ray.phi += (dλ/6.0)*(k1[1] + 2*k2[1] + 2*k3[1] + k4[1]); 210 | ray.dr += (dλ/6.0)*(k1[2] + 2*k2[2] + 2*k3[2] + k4[2]); 211 | ray.dphi += (dλ/6.0)*(k1[3] + 2*k2[3] + 2*k3[3] + k4[3]); 212 | } 213 | 214 | 215 | int main () { 216 | //rays.push_back(Ray(vec2(-1e11, 3.27606302719999999e10), vec2(c, 0.0f))); 217 | while(!glfwWindowShouldClose(engine.window)) { 218 | engine.run(); 219 | SagA.draw(); 220 | 221 | for (auto& ray : rays) { 222 | ray.step(1.0f, SagA.r_s); 223 | ray.draw(rays); 224 | } 225 | 226 | glfwSwapBuffers(engine.window); 227 | glfwPollEvents(); 228 | } 229 | 230 | return 0; 231 | } 232 | -------------------------------------------------------------------------------- /ray_tracing.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | using namespace glm; 10 | 11 | // global vars 12 | const int WIDTH = 800; 13 | const int HEIGHT = 600; 14 | 15 | // functions 16 | 17 | // structures and classes :D 18 | class Engine{ 19 | public: 20 | // -- Quad & Texture render 21 | GLFWwindow* window; 22 | GLuint quadVAO; 23 | GLuint texture; 24 | GLuint shaderProgram; 25 | 26 | Engine(){ 27 | this->window = StartGLFW(); 28 | this->shaderProgram = CreateShaderProgram(); 29 | 30 | auto result = QuadVAO(); 31 | this->quadVAO = result[0]; 32 | this->texture = result[1]; 33 | } 34 | GLFWwindow* StartGLFW(){ 35 | if(!glfwInit()){ 36 | std::cerr<<"glfw failed init, PANIC PANIC!"< QuadVAO(){ 95 | float quadVertices[] = { 96 | // positions // texCoords 97 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 98 | -1.0f, -1.0f, 0.0f, 0.0f, // bottom left 99 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 100 | 101 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 102 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 103 | 1.0f, 1.0f, 1.0f, 1.0f // top right 104 | 105 | }; 106 | 107 | GLuint VAO, VBO; 108 | glGenVertexArrays(1, &VAO); 109 | glGenBuffers(1, &VBO); 110 | 111 | glBindVertexArray(VAO); 112 | glBindBuffer(GL_ARRAY_BUFFER, VBO); 113 | glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); 114 | 115 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); 116 | glEnableVertexAttribArray(0); 117 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); 118 | glEnableVertexAttribArray(1); 119 | 120 | GLuint texture; 121 | glGenTextures(1, &texture); 122 | glBindTexture(GL_TEXTURE_2D, texture); 123 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 124 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 125 | std::vector VAOtexture = {VAO, texture}; 126 | return VAOtexture; 127 | } 128 | void renderScene(std::vector pixels) { 129 | // update texture w/ ray-tracing results 130 | glBindTexture(GL_TEXTURE_2D, texture); 131 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, 132 | GL_UNSIGNED_BYTE, pixels.data()); 133 | 134 | // clear screen and draw textured quad 135 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 136 | glUseProgram(shaderProgram); 137 | 138 | GLint textureLocation = glGetUniformLocation(shaderProgram, "screenTexture"); 139 | glUniform1i(textureLocation, 0); 140 | 141 | glBindVertexArray(quadVAO); 142 | glDrawArrays(GL_TRIANGLES, 0, 6); 143 | 144 | glfwSwapBuffers(window); 145 | glfwPollEvents(); 146 | }; 147 | }; 148 | 149 | struct Ray{ 150 | vec3 direction; 151 | vec3 origin; 152 | Ray(vec3 o, vec3 d) : origin(o), direction(normalize(d)){} 153 | }; 154 | struct Material{ 155 | vec3 color; 156 | float specular; 157 | float emission; 158 | Material(vec3 c, float s, float e) : color(c), specular(s), emission(e) {} 159 | }; 160 | struct Object{ 161 | vec3 centre; 162 | float radius; 163 | Material material; 164 | 165 | Object(vec3 c, float r, Material m) : centre(c), radius(r), material(m) {} 166 | 167 | bool Intersect(Ray &ray, float &t){ 168 | vec3 oc = ray.origin - centre; 169 | float a = glm::dot(ray.direction, ray.direction); // ray direction scale by t 170 | float b = 2.0f * glm::dot(oc, ray.direction); // 171 | float c = glm::dot(oc, oc) - radius * radius; // adjustment by sphere radius 172 | double discriminant = b*b - 4*a*c; 173 | if(discriminant < 0){return false;} // no intersection with sphere 174 | 175 | float intercept = (-b - sqrt(discriminant)) / (2.0f*a); 176 | if(intercept < 0){ 177 | intercept = (-b + sqrt(discriminant)) / (2.0f*a); 178 | if(intercept<0){return false;} // intersection is behind origin 179 | } 180 | t = intercept; 181 | return true; 182 | }; 183 | 184 | vec3 getNormal(vec3 &point) const{ 185 | return normalize(point - centre); 186 | } 187 | }; 188 | 189 | class Scene { 190 | public: 191 | std::vector objs; 192 | vec3 lightPos; 193 | Scene() : lightPos(5.0f, 5.0f, 5.0f) {} 194 | 195 | vec3 trace(Ray &ray){ 196 | float closest = INFINITY; 197 | const Object* hitObj = nullptr; 198 | 199 | for(auto& obj : objs){ 200 | float t; // distance to intersection 201 | if(obj.Intersect(ray, t)){ 202 | if(t < closest) { 203 | closest = t; 204 | hitObj = &obj; 205 | } 206 | } 207 | }; 208 | if(hitObj){ 209 | vec3 hitPoint = ray.origin + ray.direction * closest; // point on obj hit by ray 210 | vec3 normal = hitObj->getNormal(hitPoint); 211 | vec3 lightDir = normalize(lightPos - hitPoint); // direction light to hitpoint 212 | 213 | float diff = std::max(glm::dot(normal, lightDir), 0.0f); // diffuse lighting 214 | 215 | Ray shadowRay(hitPoint + normal * 0.001f, lightDir); // slightly up to avoid errors ;P 216 | // check if is in shadow 217 | bool inShadow = false; 218 | 219 | // Actually check for shadows by testing if any object blocks light 220 | for(auto& obj : objs) { 221 | float t; 222 | if(obj.Intersect(shadowRay, t)) { 223 | inShadow = true; 224 | break; 225 | } 226 | } 227 | 228 | vec3 color = hitObj->material.color; 229 | float ambient = 0.1f; // minimum light level 230 | 231 | if (inShadow) { 232 | return color * ambient; 233 | } 234 | 235 | return color * (ambient + diff * 0.9f); 236 | } 237 | 238 | return vec3(0.0f, 0.0f, 0.1f); 239 | } 240 | }; 241 | 242 | 243 | // --- main loop ---- // 244 | int main(){ 245 | Engine engine; 246 | Scene scene; 247 | 248 | scene.objs = { 249 | Object(vec3(0.0f, 0.0f, -5.0f), 2.0f, Material(vec3(1.0f, 0.2f, 0.2f), 0.5f, 0.0f)), // Moved further back and made bigger 250 | Object(vec3(3.0f, 0.0f, -7.0f), 1.5f, Material(vec3(0.2f, 1.0f, 0.2f), 0.5f, 0.0f)) // Adjusted position and size 251 | }; 252 | // -- loop -- // 253 | std::vector pixels(WIDTH * HEIGHT * 3); 254 | while(!glfwWindowShouldClose(engine.window)){ 255 | glClear(GL_COLOR_BUFFER_BIT); 256 | 257 | // render texture (pxl by pxl) 258 | for(int y = 0; y < HEIGHT; ++y){ 259 | for(int x = 0; x < WIDTH; ++x){ 260 | float aspectRatio = float(WIDTH) / float(HEIGHT); 261 | float u = float(x) / float(WIDTH); 262 | float v = float(y) / float(HEIGHT); 263 | 264 | // direction of ray threw camera 265 | vec3 direction( 266 | (2.0f * u - 1.0f) * aspectRatio, 267 | -(2.0f * v - 1.0f), // Flipped to correct orientation 268 | -1.0f // Forward direction (negative z) 269 | ); 270 | Ray ray(vec3(0.0f, 0.0f, 0.0f), normalize(direction)); 271 | vec3 color = scene.trace(ray); 272 | 273 | int index = (y * WIDTH + x) * 3; 274 | pixels[index + 0] = static_cast(color.r * 255); 275 | pixels[index + 1] = static_cast(color.g * 255); 276 | pixels[index + 2] = static_cast(color.b * 255); 277 | } 278 | } 279 | 280 | engine.renderScene(pixels); 281 | } 282 | 283 | glfwTerminate(); 284 | } 285 | 286 | 287 | // func dec's 288 | 289 | 290 | 291 | 292 | 293 | -------------------------------------------------------------------------------- /CPU-geodesic.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define _USE_MATH_DEFINES 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #ifndef M_PI 15 | #define M_PI 3.14159265358979323846 16 | #endif 17 | using namespace glm; 18 | using namespace std; 19 | using Clock = std::chrono::high_resolution_clock; 20 | 21 | // VARS 22 | double lastPrintTime = 0.0; 23 | int framesCount = 0; 24 | double c = 299792458.0; 25 | double G = 6.67430e-11; 26 | bool useGeodesics = false; 27 | 28 | struct Camera { 29 | vec3 pos; 30 | vec3 target; 31 | float fovY; 32 | float azimuth, elevation, radius; 33 | float minRadius = 1e12f, maxRadius = 1e20f; 34 | bool dragging = false; 35 | bool panning = false; 36 | double lastX = 0, lastY = 0; 37 | 38 | // Adjustable speeds 39 | float orbitSpeed = 0.008f; 40 | float panSpeed = 0.001f; 41 | float zoomSpeed = 1.08f; // closer to 1 = slower zoom 42 | 43 | Camera() : azimuth(0), elevation(M_PI / 2.0f), radius(6.34194e10), fovY(60.0f) { 44 | target = vec3(0, 0, 0); 45 | updateVectors(); 46 | } 47 | 48 | void updateVectors() { 49 | pos.x = target.x + radius * sin(elevation) * cos(azimuth); 50 | pos.y = target.y + radius * cos(elevation); 51 | pos.z = target.z + radius * sin(elevation) * sin(azimuth); 52 | } 53 | void processMouse(GLFWwindow* window, double xpos, double ypos) { 54 | float dx = float(xpos - lastX), dy = float(ypos - lastY); 55 | if (dragging && !panning) { 56 | // Orbit 57 | azimuth -= dx * orbitSpeed; 58 | elevation -= dy * orbitSpeed; 59 | elevation = glm::clamp(elevation, 0.01f, float(M_PI)-0.01f); 60 | } else if (panning) { 61 | // Pan (move target in camera plane) 62 | vec3 forward = normalize(target - pos); 63 | vec3 right = normalize(cross(forward, vec3(0,1,0))); 64 | vec3 up = cross(right, forward); 65 | target += -right * dx * panSpeed * radius + up * dy * panSpeed * radius; 66 | } 67 | updateVectors(); 68 | lastX = xpos; lastY = ypos; 69 | } 70 | void processScroll(double yoffset) { 71 | // Zoom (dolly in/out) 72 | if (yoffset < 0) 73 | radius *= pow(zoomSpeed, -yoffset); 74 | else 75 | radius /= pow(zoomSpeed, yoffset); 76 | radius = glm::clamp(radius, minRadius, maxRadius); 77 | updateVectors(); 78 | } 79 | static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { 80 | Camera* cam = (Camera*)glfwGetWindowUserPointer(window); 81 | if (button == GLFW_MOUSE_BUTTON_LEFT) { 82 | if (action == GLFW_PRESS) { 83 | cam->dragging = true; 84 | cam->panning = (mods & GLFW_MOD_SHIFT); 85 | double x, y; glfwGetCursorPos(window, &x, &y); 86 | cam->lastX = x; cam->lastY = y; 87 | } else if (action == GLFW_RELEASE) { 88 | cam->dragging = false; 89 | cam->panning = false; 90 | } 91 | } 92 | } 93 | static void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) { 94 | Camera* cam = (Camera*)glfwGetWindowUserPointer(window); 95 | cam->processMouse(window, xpos, ypos); 96 | } 97 | static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset) { 98 | Camera* cam = (Camera*)glfwGetWindowUserPointer(window); 99 | cam->processScroll(yoffset); 100 | } 101 | }; 102 | Camera camera; 103 | 104 | struct Ray; 105 | void rk4Step(Ray& ray, double dλ, double rs); 106 | 107 | struct Engine { 108 | // -- Quad & Texture render -- // 109 | GLFWwindow* window; 110 | GLuint quadVAO; 111 | GLuint texture; 112 | GLuint shaderProgram; 113 | int WIDTH = 800; 114 | int HEIGHT = 600; 115 | float width = 100000000000.0f; // Width of the viewport in meters 116 | float height = 75000000000.0f; // Height of the viewport in meters 117 | 118 | Engine() { 119 | if (!glfwInit()) { 120 | cerr << "GLFW init failed\n"; 121 | exit(EXIT_FAILURE); 122 | } 123 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 124 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 125 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 126 | window = glfwCreateWindow(WIDTH, HEIGHT, "Black Hole", nullptr, nullptr); 127 | if (!window) { 128 | cerr << "Failed to create GLFW window\n"; 129 | glfwTerminate(); 130 | exit(EXIT_FAILURE); 131 | } 132 | glfwMakeContextCurrent(window); 133 | glewExperimental = GL_TRUE; 134 | GLenum glewErr = glewInit(); 135 | if (glewErr != GLEW_OK) { 136 | cerr << "Failed to initialize GLEW: " 137 | << (const char*)glewGetErrorString(glewErr) 138 | << "\n"; 139 | glfwTerminate(); 140 | exit(EXIT_FAILURE); 141 | } 142 | cout << "OpenGL " << glGetString(GL_VERSION) << "\n"; 143 | this->shaderProgram = CreateShaderProgram(); 144 | 145 | auto result = QuadVAO(); 146 | this->quadVAO = result[0]; 147 | this->texture = result[1]; 148 | } 149 | GLuint CreateShaderProgram(){ 150 | const char* vertexShaderSource = R"( 151 | #version 330 core 152 | layout (location = 0) in vec2 aPos; // Changed to vec2 153 | layout (location = 1) in vec2 aTexCoord; 154 | out vec2 TexCoord; 155 | void main() { 156 | gl_Position = vec4(aPos, 0.0, 1.0); // Explicit z=0 157 | TexCoord = aTexCoord; 158 | })"; 159 | 160 | const char* fragmentShaderSource = R"( 161 | #version 330 core 162 | in vec2 TexCoord; 163 | out vec4 FragColor; 164 | uniform sampler2D screenTexture; 165 | void main() { 166 | FragColor = texture(screenTexture, TexCoord); 167 | })"; 168 | 169 | // vertex shader 170 | GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); 171 | glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); 172 | glCompileShader(vertexShader); 173 | 174 | // fragment shader 175 | GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 176 | glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); 177 | glCompileShader(fragmentShader); 178 | 179 | GLuint shaderProgram = glCreateProgram(); 180 | glAttachShader(shaderProgram, vertexShader); 181 | glAttachShader(shaderProgram, fragmentShader); 182 | glLinkProgram(shaderProgram); 183 | 184 | glDeleteShader(vertexShader); 185 | glDeleteShader(fragmentShader); 186 | 187 | return shaderProgram; 188 | }; 189 | vector QuadVAO(){ 190 | float quadVertices[] = { 191 | // positions // texCoords 192 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 193 | -1.0f, -1.0f, 0.0f, 0.0f, // bottom left 194 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 195 | 196 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 197 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 198 | 1.0f, 1.0f, 1.0f, 1.0f // top right 199 | 200 | }; 201 | 202 | GLuint VAO, VBO; 203 | glGenVertexArrays(1, &VAO); 204 | glGenBuffers(1, &VBO); 205 | 206 | glBindVertexArray(VAO); 207 | glBindBuffer(GL_ARRAY_BUFFER, VBO); 208 | glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); 209 | 210 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); 211 | glEnableVertexAttribArray(0); 212 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); 213 | glEnableVertexAttribArray(1); 214 | 215 | GLuint texture; 216 | glGenTextures(1, &texture); 217 | glBindTexture(GL_TEXTURE_2D, texture); 218 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 219 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 220 | vector VAOtexture = {VAO, texture}; 221 | return VAOtexture; 222 | } 223 | void renderScene(const vector& pixels, int texWidth, int texHeight) { 224 | // update texture w/ ray-tracing results 225 | glBindTexture(GL_TEXTURE_2D, texture); 226 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texWidth, texHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); 227 | 228 | // clear screen and draw textured quad 229 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 230 | glUseProgram(shaderProgram); 231 | 232 | GLint textureLocation = glGetUniformLocation(shaderProgram, "screenTexture"); 233 | glUniform1i(textureLocation, 0); 234 | 235 | glBindVertexArray(quadVAO); 236 | glDrawArrays(GL_TRIANGLES, 0, 6); 237 | 238 | glfwSwapBuffers(window); 239 | glfwPollEvents(); 240 | }; 241 | static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { 242 | if (action == GLFW_PRESS) { 243 | if (key == GLFW_KEY_G) { 244 | useGeodesics = !useGeodesics; 245 | cout << "Geodesics: " << (useGeodesics ? "ON\n" : "OFF\n"); 246 | } 247 | } 248 | } 249 | }; 250 | Engine engine; 251 | struct BlackHole { 252 | vec3 position; 253 | double mass; 254 | double radius; 255 | double r_s; 256 | 257 | BlackHole(vec3 pos, float m) : position(pos), mass(m) {r_s = 2.0 * G * mass / (c*c);} 258 | bool Intercept(float px, float py, float pz) const { 259 | float dx = px - position.x; 260 | float dy = py - position.y; 261 | float dz = pz - position.z; 262 | float dist2 = dx * dx + dy * dy + dz * dz; 263 | return dist2 < r_s * r_s; 264 | } 265 | }; 266 | BlackHole SagA(vec3(0.0f, 0.0f, 0.0f), 8.54e36); // Sagittarius A black hole 267 | struct Ray{ 268 | // -- cartesian coords -- // 269 | double x; double y; double z; 270 | // -- polar coords -- // 271 | double r; double phi; double theta; 272 | double dr; double dphi; double dtheta; 273 | double E, L; // conserved quantities 274 | 275 | Ray(vec3 pos, vec3 dir) : x(pos.x), y(pos.y), z(pos.z) { 276 | // Step 1: get spherical coords (r, theta, phi) 277 | r = sqrt(x*x + y*y + z*z); 278 | theta = acos(z / r); 279 | phi = atan2(y, x); 280 | 281 | // Step 2: seed velocities (dr, dtheta, dphi) 282 | // Convert direction to spherical basis 283 | double dx = dir.x, dy = dir.y, dz = dir.z; 284 | dr = sin(theta)*cos(phi)*dx + sin(theta)*sin(phi)*dy + cos(theta)*dz; 285 | dtheta = cos(theta)*cos(phi)*dx + cos(theta)*sin(phi)*dy - sin(theta)*dz; 286 | dtheta /= r; 287 | dphi = -sin(phi)*dx + cos(phi)*dy; 288 | dphi /= (r * sin(theta)); 289 | 290 | // Step 3: store conserved quantities 291 | L = r * r * sin(theta) * dphi; 292 | double f = 1.0 - SagA.r_s / r; 293 | double dt_dλ = sqrt((dr*dr)/f + r*r*dtheta*dtheta + r*r*sin(theta)*sin(theta)*dphi*dphi); 294 | E = f * dt_dλ; 295 | } 296 | void step(double dλ, double rs) { 297 | if (r <= rs) return; 298 | rk4Step(*this, dλ, rs); 299 | // convert back to cartesian 300 | this->x = r * sin(theta) * cos(phi); 301 | this->y = r * sin(theta) * sin(phi); 302 | this->z = r * cos(theta); 303 | } 304 | }; 305 | 306 | void raytrace(vector& pixels, int W, int H) { 307 | pixels.resize(W * H * 3); 308 | 309 | // build camera basis 310 | vec3 forward = normalize(camera.target - camera.pos); 311 | vec3 right = normalize(cross(forward, vec3(0,1,0))); 312 | vec3 up = cross(right, forward); 313 | float aspect = float(W) / float(H); 314 | float tanHalfFov = tan(radians(camera.fovY) * 0.5f); 315 | 316 | #pragma omp parallel for schedule(dynamic, 4) 317 | for(int y = 0; y < H; ++y) { 318 | for(int x = 0; x < W; ++x) { 319 | // NDC → screen space in [−1,1] 320 | float u = (2.0f * (x + 0.5f) / float(W) - 1.0f) * aspect * tanHalfFov; 321 | float v = (1.0f - 2.0f * (y + 0.5f) / float(H)) * tanHalfFov; 322 | vec3 dir = normalize(u*right + v*up + forward); 323 | 324 | // construct your Ray 325 | Ray ray(camera.pos, dir); 326 | 327 | const int MAX_STEPS = 10000; 328 | const double D_LAMBDA = 1e7; 329 | const double ESCAPE_R = 1e14; 330 | 331 | // 2) march the ray forward in λ 332 | vec3 color(0.0f); 333 | if (!useGeodesics) { 334 | double b = 2.0 * dot(camera.pos, dir); 335 | double c0 = dot(camera.pos, camera.pos) - SagA.r_s*SagA.r_s; 336 | double disc = b*b - 4.0*c0; 337 | if (disc > 0.0) { 338 | double t1 = (-b - sqrt(disc)) * 0.5; 339 | double t2 = (-b + sqrt(disc)) * 0.5; 340 | if (t1 > 0.0 || t2 > 0.0) 341 | color = vec3(1.0f, 0.0f, 0.0f); 342 | } 343 | } 344 | else { 345 | // full null‐geodesic march 346 | Ray ray(camera.pos, dir); 347 | for(int i = 0; i < MAX_STEPS; ++i) { 348 | if (SagA.Intercept(ray.x, ray.y, ray.z)) { 349 | color = vec3(1.0f, 0.0f, 0.0f); 350 | break; 351 | } 352 | ray.step(D_LAMBDA, SagA.r_s); 353 | if (ray.r > ESCAPE_R) { 354 | // escaped to infinity → remains black 355 | break; 356 | } 357 | } 358 | } 359 | 360 | int idx = (y * W + x) * 3; 361 | pixels[idx+0] = (unsigned char)(color.r * 255); 362 | pixels[idx+1] = (unsigned char)(color.g * 255); 363 | pixels[idx+2] = (unsigned char)(color.b * 255); 364 | } 365 | } 366 | } 367 | 368 | void geodesicRHS(const Ray& ray, double rhs[6], double rs) { 369 | double r = ray.r; 370 | double theta = ray.theta; 371 | double dr = ray.dr; 372 | double dtheta = ray.dtheta; 373 | double dphi = ray.dphi; 374 | double E = ray.E; 375 | 376 | double f = 1.0 - rs / r; 377 | double dt_dlambda = E / f; 378 | 379 | // First derivatives 380 | rhs[0] = dr; 381 | rhs[1] = dtheta; 382 | rhs[2] = dphi; 383 | 384 | // Second derivatives (from 3D Schwarzschild null geodesics): 385 | rhs[3] = 386 | - (rs / (2 * r * r)) * f * dt_dlambda * dt_dlambda 387 | + (rs / (2 * r * r * f)) * dr * dr 388 | + r * (dtheta * dtheta + sin(theta) * sin(theta) * dphi * dphi); 389 | 390 | rhs[4] = 391 | - (2.0 / r) * dr * dtheta 392 | + sin(theta) * cos(theta) * dphi * dphi; 393 | 394 | rhs[5] = 395 | - (2.0 / r) * dr * dphi 396 | - 2.0 * cos(theta) / sin(theta) * dtheta * dphi; 397 | } 398 | void addState(const double a[6], const double b[6], double factor, double out[6]) { 399 | for (int i = 0; i < 6; i++) 400 | out[i] = a[i] + b[i] * factor; 401 | } 402 | void rk4Step(Ray& ray, double dλ, double rs) { 403 | double y0[6] = { ray.r, ray.theta, ray.phi, ray.dr, ray.dtheta, ray.dphi }; 404 | double k1[6], k2[6], k3[6], k4[6], temp[6]; 405 | 406 | geodesicRHS(ray, k1, rs); 407 | addState(y0, k1, dλ/2.0, temp); 408 | Ray r2 = ray; 409 | r2.r = temp[0]; r2.theta = temp[1]; r2.phi = temp[2]; 410 | r2.dr = temp[3]; r2.dtheta = temp[4]; r2.dphi = temp[5]; 411 | geodesicRHS(r2, k2, rs); 412 | 413 | addState(y0, k2, dλ/2.0, temp); 414 | Ray r3 = ray; 415 | r3.r = temp[0]; r3.theta = temp[1]; r3.phi = temp[2]; 416 | r3.dr = temp[3]; r3.dtheta = temp[4]; r3.dphi = temp[5]; 417 | geodesicRHS(r3, k3, rs); 418 | 419 | addState(y0, k3, dλ, temp); 420 | Ray r4 = ray; 421 | r4.r = temp[0]; r4.theta = temp[1]; r4.phi = temp[2]; 422 | r4.dr = temp[3]; r4.dtheta = temp[4]; r4.dphi = temp[5]; 423 | geodesicRHS(r4, k4, rs); 424 | 425 | ray.r += (dλ/6.0)*(k1[0] + 2*k2[0] + 2*k3[0] + k4[0]); 426 | ray.theta += (dλ/6.0)*(k1[1] + 2*k2[1] + 2*k3[1] + k4[1]); 427 | ray.phi += (dλ/6.0)*(k1[2] + 2*k2[2] + 2*k3[2] + k4[2]); 428 | ray.dr += (dλ/6.0)*(k1[3] + 2*k2[3] + 2*k3[3] + k4[3]); 429 | ray.dtheta += (dλ/6.0)*(k1[4] + 2*k2[4] + 2*k3[4] + k4[4]); 430 | ray.dphi += (dλ/6.0)*(k1[5] + 2*k2[5] + 2*k3[5] + k4[5]); 431 | } 432 | 433 | void setupCameraCallbacks(GLFWwindow* window) { 434 | glfwSetWindowUserPointer(window, &camera); 435 | glfwSetMouseButtonCallback(window, Camera::mouseButtonCallback); 436 | glfwSetCursorPosCallback(window, Camera::cursorPosCallback); 437 | glfwSetScrollCallback(window, Camera::scrollCallback); 438 | glfwSetKeyCallback(window, Engine::keyCallback); 439 | } 440 | 441 | // -- MAIN -- // 442 | int main() { 443 | setupCameraCallbacks(engine.window); 444 | vector pixels(engine.WIDTH * engine.HEIGHT * 3); 445 | 446 | auto t0 = Clock::now(); 447 | lastPrintTime = std::chrono::duration(t0.time_since_epoch()).count(); 448 | 449 | while (!glfwWindowShouldClose(engine.window)) { 450 | raytrace(pixels, engine.WIDTH, engine.HEIGHT); 451 | engine.renderScene(pixels, engine.WIDTH, engine.HEIGHT); 452 | 453 | // 2) FPS counting 454 | framesCount++; 455 | auto t1 = Clock::now(); 456 | double now = std::chrono::duration(t1.time_since_epoch()).count(); 457 | if (now - lastPrintTime >= 1.0) { 458 | cout << "FPS: " << framesCount / (now - lastPrintTime) << "\n"; 459 | framesCount = 0; 460 | lastPrintTime = now; 461 | } 462 | 463 | } 464 | 465 | glfwDestroyWindow(engine.window); 466 | glfwTerminate(); 467 | return 0; 468 | } 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | // 2) FPS counting 488 | // framesCount++; 489 | // auto t1 = Clock::now(); 490 | // double now = std::chrono::duration(t1.time_since_epoch()).count(); 491 | // if (now - lastPrintTime >= 1.0) { 492 | // cout << "FPS: " << framesCount / (now - lastPrintTime) << "\n"; 493 | // framesCount = 0; 494 | // lastPrintTime = now; 495 | // } 496 | //raytrace(pixels, engine.WIDTH, engine.HEIGHT); 497 | -------------------------------------------------------------------------------- /black_hole.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #define _USE_MATH_DEFINES 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #ifndef M_PI 17 | #define M_PI 3.14159265358979323846 18 | #endif 19 | using namespace glm; 20 | using namespace std; 21 | using Clock = std::chrono::high_resolution_clock; 22 | 23 | // VARS 24 | double lastPrintTime = 0.0; 25 | int framesCount = 0; 26 | double c = 299792458.0; 27 | double G = 6.67430e-11; 28 | struct Ray; 29 | bool Gravity = false; 30 | 31 | struct Camera { 32 | // Center the camera orbit on the black hole at (0, 0, 0) 33 | vec3 target = vec3(0.0f, 0.0f, 0.0f); // Always look at the black hole center 34 | float radius = 6.34194e10f; 35 | float minRadius = 1e10f, maxRadius = 1e12f; 36 | 37 | float azimuth = 0.0f; 38 | float elevation = M_PI / 2.0f; 39 | 40 | float orbitSpeed = 0.01f; 41 | float panSpeed = 0.01f; 42 | double zoomSpeed = 25e9f; 43 | 44 | bool dragging = false; 45 | bool panning = false; 46 | bool moving = false; // For compute shader optimization 47 | double lastX = 0.0, lastY = 0.0; 48 | 49 | // Calculate camera position in world space 50 | vec3 position() const { 51 | float clampedElevation = glm::clamp(elevation, 0.01f, float(M_PI) - 0.01f); 52 | // Orbit around (0,0,0) always 53 | return vec3( 54 | radius * sin(clampedElevation) * cos(azimuth), 55 | radius * cos(clampedElevation), 56 | radius * sin(clampedElevation) * sin(azimuth) 57 | ); 58 | } 59 | void update() { 60 | // Always keep target at black hole center 61 | target = vec3(0.0f, 0.0f, 0.0f); 62 | if(dragging | panning) { 63 | moving = true; 64 | } else { 65 | moving = false; 66 | } 67 | } 68 | 69 | void processMouseMove(double x, double y) { 70 | float dx = float(x - lastX); 71 | float dy = float(y - lastY); 72 | 73 | if (dragging && panning) { 74 | // Pan: Shift + Left or Middle Mouse 75 | // Disable panning to keep camera centered on black hole 76 | } 77 | else if (dragging && !panning) { 78 | // Orbit: Left mouse only 79 | azimuth += dx * orbitSpeed; 80 | elevation -= dy * orbitSpeed; 81 | elevation = glm::clamp(elevation, 0.01f, float(M_PI) - 0.01f); 82 | } 83 | 84 | lastX = x; 85 | lastY = y; 86 | update(); 87 | } 88 | void processMouseButton(int button, int action, int mods, GLFWwindow* win) { 89 | if (button == GLFW_MOUSE_BUTTON_LEFT || button == GLFW_MOUSE_BUTTON_MIDDLE) { 90 | if (action == GLFW_PRESS) { 91 | dragging = true; 92 | // Disable panning so camera always orbits center 93 | panning = false; 94 | glfwGetCursorPos(win, &lastX, &lastY); 95 | } else if (action == GLFW_RELEASE) { 96 | dragging = false; 97 | panning = false; 98 | } 99 | } 100 | if (button == GLFW_MOUSE_BUTTON_RIGHT) { 101 | if (action == GLFW_PRESS) { 102 | Gravity = true; 103 | } else if (action == GLFW_RELEASE) { 104 | Gravity = false; 105 | } 106 | } 107 | } 108 | void processScroll(double xoffset, double yoffset) { 109 | radius -= yoffset * zoomSpeed; 110 | radius = glm::clamp(radius, minRadius, maxRadius); 111 | update(); 112 | } 113 | void processKey(int key, int scancode, int action, int mods) { 114 | if (action == GLFW_PRESS && key == GLFW_KEY_G) { 115 | Gravity = !Gravity; 116 | cout << "[INFO] Gravity turned " << (Gravity ? "ON" : "OFF") << endl; 117 | } 118 | } 119 | }; 120 | Camera camera; 121 | 122 | struct BlackHole { 123 | vec3 position; 124 | double mass; 125 | double radius; 126 | double r_s; 127 | 128 | BlackHole(vec3 pos, float m) : position(pos), mass(m) {r_s = 2.0 * G * mass / (c*c);} 129 | bool Intercept(float px, float py, float pz) const { 130 | double dx = double(px) - double(position.x); 131 | double dy = double(py) - double(position.y); 132 | double dz = double(pz) - double(position.z); 133 | double dist2 = dx * dx + dy * dy + dz * dz; 134 | return dist2 < r_s * r_s; 135 | } 136 | }; 137 | BlackHole SagA(vec3(0.0f, 0.0f, 0.0f), 8.54e36); // Sagittarius A black hole 138 | struct ObjectData { 139 | vec4 posRadius; // xyz = position, w = radius 140 | vec4 color; // rgb = color, a = unused 141 | float mass; 142 | vec3 velocity = vec3(0.0f, 0.0f, 0.0f); // Initial velocity 143 | }; 144 | vector objects = { 145 | { vec4(4e11f, 0.0f, 0.0f, 4e10f) , vec4(1,1,0,1), 1.98892e30 }, 146 | { vec4(0.0f, 0.0f, 4e11f, 4e10f) , vec4(1,0,0,1), 1.98892e30 }, 147 | { vec4(0.0f, 0.0f, 0.0f, SagA.r_s) , vec4(0,0,0,1), static_cast(SagA.mass) }, 148 | //{ vec4(6e10f, 0.0f, 0.0f, 5e10f), vec4(0,1,0,1) } 149 | }; 150 | 151 | struct Engine { 152 | GLuint gridShaderProgram; 153 | // -- Quad & Texture render -- // 154 | GLFWwindow* window; 155 | GLuint quadVAO; 156 | GLuint texture; 157 | GLuint shaderProgram; 158 | GLuint computeProgram = 0; 159 | // -- UBOs -- // 160 | GLuint cameraUBO = 0; 161 | GLuint diskUBO = 0; 162 | GLuint objectsUBO = 0; 163 | // -- grid mess vars -- // 164 | GLuint gridVAO = 0; 165 | GLuint gridVBO = 0; 166 | GLuint gridEBO = 0; 167 | int gridIndexCount = 0; 168 | 169 | int WIDTH = 800; // Window width 170 | int HEIGHT = 600; // Window height 171 | int COMPUTE_WIDTH = 200; // Compute resolution width 172 | int COMPUTE_HEIGHT = 150; // Compute resolution height 173 | float width = 100000000000.0f; // Width of the viewport in meters 174 | float height = 75000000000.0f; // Height of the viewport in meters 175 | 176 | Engine() { 177 | if (!glfwInit()) { 178 | cerr << "GLFW init failed\n"; 179 | exit(EXIT_FAILURE); 180 | } 181 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); 182 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 183 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 184 | window = glfwCreateWindow(WIDTH, HEIGHT, "Black Hole", nullptr, nullptr); 185 | if (!window) { 186 | cerr << "Failed to create GLFW window\n"; 187 | glfwTerminate(); 188 | exit(EXIT_FAILURE); 189 | } 190 | glfwMakeContextCurrent(window); 191 | glewExperimental = GL_TRUE; 192 | GLenum glewErr = glewInit(); 193 | if (glewErr != GLEW_OK) { 194 | cerr << "Failed to initialize GLEW: " 195 | << (const char*)glewGetErrorString(glewErr) 196 | << "\n"; 197 | glfwTerminate(); 198 | exit(EXIT_FAILURE); 199 | } 200 | cout << "OpenGL " << glGetString(GL_VERSION) << "\n"; 201 | this->shaderProgram = CreateShaderProgram(); 202 | gridShaderProgram = CreateShaderProgram("grid.vert", "grid.frag"); 203 | 204 | computeProgram = CreateComputeProgram("geodesic.comp"); 205 | glGenBuffers(1, &cameraUBO); 206 | glBindBuffer(GL_UNIFORM_BUFFER, cameraUBO); 207 | glBufferData(GL_UNIFORM_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW); // alloc ~128 bytes 208 | glBindBufferBase(GL_UNIFORM_BUFFER, 1, cameraUBO); // binding = 1 matches shader 209 | 210 | glGenBuffers(1, &diskUBO); 211 | glBindBuffer(GL_UNIFORM_BUFFER, diskUBO); 212 | glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * 4, nullptr, GL_DYNAMIC_DRAW); // 3 values + 1 padding 213 | glBindBufferBase(GL_UNIFORM_BUFFER, 2, diskUBO); // binding = 2 matches compute shader 214 | 215 | glGenBuffers(1, &objectsUBO); 216 | glBindBuffer(GL_UNIFORM_BUFFER, objectsUBO); 217 | // allocate space for 16 objects: 218 | // sizeof(int) + padding + 16×(vec4 posRadius + vec4 color) 219 | GLsizeiptr objUBOSize = sizeof(int) + 3 * sizeof(float) 220 | + 16 * (sizeof(vec4) + sizeof(vec4)) 221 | + 16 * sizeof(float); // 16 floats for mass 222 | glBufferData(GL_UNIFORM_BUFFER, objUBOSize, nullptr, GL_DYNAMIC_DRAW); 223 | glBindBufferBase(GL_UNIFORM_BUFFER, 3, objectsUBO); // binding = 3 matches shader 224 | 225 | auto result = QuadVAO(); 226 | this->quadVAO = result[0]; 227 | this->texture = result[1]; 228 | } 229 | void generateGrid(const vector& objects) { 230 | const int gridSize = 25; 231 | const float spacing = 1e10f; // tweak this 232 | 233 | vector vertices; 234 | vector indices; 235 | 236 | for (int z = 0; z <= gridSize; ++z) { 237 | for (int x = 0; x <= gridSize; ++x) { 238 | float worldX = (x - gridSize / 2) * spacing; 239 | float worldZ = (z - gridSize / 2) * spacing; 240 | 241 | float y = 0.0f; 242 | 243 | // ✅ Warp grid using Schwarzschild geometry 244 | for (const auto& obj : objects) { 245 | vec3 objPos = vec3(obj.posRadius); 246 | double mass = obj.mass; 247 | double radius = obj.posRadius.w; 248 | 249 | double r_s = 2.0 * G * mass / (c * c); 250 | double dx = worldX - objPos.x; 251 | double dz = worldZ - objPos.z; 252 | double dist = sqrt(dx * dx + dz * dz); 253 | 254 | // prevent sqrt of negative or divide-by-zero (inside or at the black hole center) 255 | if (dist > r_s) { 256 | double deltaY = 2.0 * sqrt(r_s * (dist - r_s)); 257 | y += static_cast(deltaY) - 3e10f; 258 | } else { 259 | // 🔴 For points inside or at r_s: make it dip down sharply 260 | y += 2.0f * static_cast(sqrt(r_s * r_s)) - 3e10f; // or add a deep pit 261 | } 262 | } 263 | 264 | vertices.emplace_back(worldX, y, worldZ); 265 | } 266 | } 267 | 268 | // 🧩 Add indices for GL_LINE rendering 269 | for (int z = 0; z < gridSize; ++z) { 270 | for (int x = 0; x < gridSize; ++x) { 271 | int i = z * (gridSize + 1) + x; 272 | indices.push_back(i); 273 | indices.push_back(i + 1); 274 | 275 | indices.push_back(i); 276 | indices.push_back(i + gridSize + 1); 277 | } 278 | } 279 | 280 | // 🔌 Upload to GPU 281 | if (gridVAO == 0) glGenVertexArrays(1, &gridVAO); 282 | if (gridVBO == 0) glGenBuffers(1, &gridVBO); 283 | if (gridEBO == 0) glGenBuffers(1, &gridEBO); 284 | 285 | glBindVertexArray(gridVAO); 286 | 287 | glBindBuffer(GL_ARRAY_BUFFER, gridVBO); 288 | glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vec3), vertices.data(), GL_DYNAMIC_DRAW); 289 | 290 | glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gridEBO); 291 | glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(GLuint), indices.data(), GL_STATIC_DRAW); 292 | 293 | glEnableVertexAttribArray(0); // location = 0 294 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vec3), (void*)0); 295 | 296 | gridIndexCount = indices.size(); 297 | 298 | glBindVertexArray(0); 299 | } 300 | void drawGrid(const mat4& viewProj) { 301 | glUseProgram(gridShaderProgram); 302 | glUniformMatrix4fv(glGetUniformLocation(gridShaderProgram, "viewProj"), 303 | 1, GL_FALSE, glm::value_ptr(viewProj)); 304 | glBindVertexArray(gridVAO); 305 | 306 | glDisable(GL_DEPTH_TEST); 307 | glEnable(GL_BLEND); 308 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 309 | 310 | glDrawElements(GL_LINES, gridIndexCount, GL_UNSIGNED_INT, 0); 311 | 312 | glBindVertexArray(0); 313 | glEnable(GL_DEPTH_TEST); 314 | } 315 | void drawFullScreenQuad() { 316 | glUseProgram(shaderProgram); // fragment + vertex shader 317 | glBindVertexArray(quadVAO); 318 | 319 | glActiveTexture(GL_TEXTURE0); 320 | glBindTexture(GL_TEXTURE_2D, texture); 321 | glUniform1i(glGetUniformLocation(shaderProgram, "screenTexture"), 0); 322 | 323 | glDisable(GL_DEPTH_TEST); // draw as background 324 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 6); // 2 triangles 325 | glEnable(GL_DEPTH_TEST); 326 | } 327 | GLuint CreateShaderProgram(){ 328 | const char* vertexShaderSource = R"( 329 | #version 330 core 330 | layout (location = 0) in vec2 aPos; // Changed to vec2 331 | layout (location = 1) in vec2 aTexCoord; 332 | out vec2 TexCoord; 333 | void main() { 334 | gl_Position = vec4(aPos, 0.0, 1.0); // Explicit z=0 335 | TexCoord = aTexCoord; 336 | })"; 337 | 338 | const char* fragmentShaderSource = R"( 339 | #version 330 core 340 | in vec2 TexCoord; 341 | out vec4 FragColor; 342 | uniform sampler2D screenTexture; 343 | void main() { 344 | FragColor = texture(screenTexture, TexCoord); 345 | })"; 346 | 347 | // vertex shader 348 | GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); 349 | glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr); 350 | glCompileShader(vertexShader); 351 | 352 | // fragment shader 353 | GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 354 | glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr); 355 | glCompileShader(fragmentShader); 356 | 357 | GLuint shaderProgram = glCreateProgram(); 358 | glAttachShader(shaderProgram, vertexShader); 359 | glAttachShader(shaderProgram, fragmentShader); 360 | glLinkProgram(shaderProgram); 361 | 362 | glDeleteShader(vertexShader); 363 | glDeleteShader(fragmentShader); 364 | 365 | return shaderProgram; 366 | }; 367 | GLuint CreateShaderProgram(const char* vertPath, const char* fragPath) { 368 | auto loadShader = [](const char* path, GLenum type) -> GLuint { 369 | std::ifstream in(path); 370 | if (!in.is_open()) { 371 | std::cerr << "Failed to open shader: " << path << "\n"; 372 | exit(EXIT_FAILURE); 373 | } 374 | std::stringstream ss; 375 | ss << in.rdbuf(); 376 | std::string srcStr = ss.str(); 377 | const char* src = srcStr.c_str(); 378 | 379 | GLuint shader = glCreateShader(type); 380 | glShaderSource(shader, 1, &src, nullptr); 381 | glCompileShader(shader); 382 | 383 | GLint success; 384 | glGetShaderiv(shader, GL_COMPILE_STATUS, &success); 385 | if (!success) { 386 | GLint logLen; 387 | glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLen); 388 | std::vector log(logLen); 389 | glGetShaderInfoLog(shader, logLen, nullptr, log.data()); 390 | std::cerr << "Shader compile error (" << path << "):\n" << log.data() << "\n"; 391 | exit(EXIT_FAILURE); 392 | } 393 | return shader; 394 | }; 395 | 396 | GLuint vertShader = loadShader(vertPath, GL_VERTEX_SHADER); 397 | GLuint fragShader = loadShader(fragPath, GL_FRAGMENT_SHADER); 398 | 399 | GLuint program = glCreateProgram(); 400 | glAttachShader(program, vertShader); 401 | glAttachShader(program, fragShader); 402 | glLinkProgram(program); 403 | 404 | GLint linkSuccess; 405 | glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess); 406 | if (!linkSuccess) { 407 | GLint logLen; 408 | glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLen); 409 | std::vector log(logLen); 410 | glGetProgramInfoLog(program, logLen, nullptr, log.data()); 411 | std::cerr << "Shader link error:\n" << log.data() << "\n"; 412 | exit(EXIT_FAILURE); 413 | } 414 | 415 | glDeleteShader(vertShader); 416 | glDeleteShader(fragShader); 417 | 418 | return program; 419 | } 420 | GLuint CreateComputeProgram(const char* path) { 421 | // 1) read GLSL source 422 | std::ifstream in(path); 423 | if(!in.is_open()) { 424 | std::cerr << "Failed to open compute shader: " << path << "\n"; 425 | exit(EXIT_FAILURE); 426 | } 427 | std::stringstream ss; 428 | ss << in.rdbuf(); 429 | std::string srcStr = ss.str(); 430 | const char* src = srcStr.c_str(); 431 | 432 | // 2) compile 433 | GLuint cs = glCreateShader(GL_COMPUTE_SHADER); 434 | glShaderSource(cs, 1, &src, nullptr); 435 | glCompileShader(cs); 436 | GLint ok; 437 | glGetShaderiv(cs, GL_COMPILE_STATUS, &ok); 438 | if(!ok) { 439 | GLint logLen; 440 | glGetShaderiv(cs, GL_INFO_LOG_LENGTH, &logLen); 441 | std::vector log(logLen); 442 | glGetShaderInfoLog(cs, logLen, nullptr, log.data()); 443 | std::cerr << "Compute shader compile error:\n" << log.data() << "\n"; 444 | exit(EXIT_FAILURE); 445 | } 446 | 447 | // 3) link 448 | GLuint prog = glCreateProgram(); 449 | glAttachShader(prog, cs); 450 | glLinkProgram(prog); 451 | glGetProgramiv(prog, GL_LINK_STATUS, &ok); 452 | if(!ok) { 453 | GLint logLen; 454 | glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLen); 455 | std::vector log(logLen); 456 | glGetProgramInfoLog(prog, logLen, nullptr, log.data()); 457 | std::cerr << "Compute shader link error:\n" << log.data() << "\n"; 458 | exit(EXIT_FAILURE); 459 | } 460 | 461 | glDeleteShader(cs); 462 | return prog; 463 | } 464 | void dispatchCompute(const Camera& cam) { 465 | // determine target compute‐res 466 | int cw = cam.moving ? COMPUTE_WIDTH : 200; 467 | int ch = cam.moving ? COMPUTE_HEIGHT : 150; 468 | 469 | // 1) reallocate the texture if needed 470 | glBindTexture(GL_TEXTURE_2D, texture); 471 | glTexImage2D(GL_TEXTURE_2D, 472 | 0, // mip 473 | GL_RGBA8, // internal format 474 | cw, // width 475 | ch, // height 476 | 0, GL_RGBA, 477 | GL_UNSIGNED_BYTE, 478 | nullptr); 479 | 480 | // 2) bind compute program & UBOs 481 | glUseProgram(computeProgram); 482 | uploadCameraUBO(cam); 483 | uploadDiskUBO(); 484 | uploadObjectsUBO(objects); 485 | 486 | // 3) bind it as image unit 0 487 | glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); 488 | 489 | // 4) dispatch grid 490 | GLuint groupsX = (GLuint)std::ceil(cw / 16.0f); 491 | GLuint groupsY = (GLuint)std::ceil(ch / 16.0f); 492 | glDispatchCompute(groupsX, groupsY, 1); 493 | 494 | // 5) sync 495 | glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); 496 | } 497 | void uploadCameraUBO(const Camera& cam) { 498 | struct UBOData { 499 | vec3 pos; float _pad0; 500 | vec3 right; float _pad1; 501 | vec3 up; float _pad2; 502 | vec3 forward; float _pad3; 503 | float tanHalfFov; 504 | float aspect; 505 | bool moving; 506 | int _pad4; 507 | } data; 508 | vec3 fwd = normalize(cam.target - cam.position()); 509 | vec3 up = vec3(0, 1, 0); // y axis is up, so disk is in x-z plane 510 | vec3 right = normalize(cross(fwd, up)); 511 | up = cross(right, fwd); 512 | 513 | data.pos = cam.position(); 514 | data.right = right; 515 | data.up = up; 516 | data.forward = fwd; 517 | data.tanHalfFov = tan(radians(60.0f * 0.5f)); 518 | data.aspect = float(WIDTH) / float(HEIGHT); 519 | data.moving = cam.dragging || cam.panning; 520 | 521 | glBindBuffer(GL_UNIFORM_BUFFER, cameraUBO); 522 | glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(UBOData), &data); 523 | } 524 | void uploadObjectsUBO(const vector& objs) { 525 | struct UBOData { 526 | int numObjects; 527 | float _pad0, _pad1, _pad2; // <-- pad out to 16 bytes 528 | vec4 posRadius[16]; 529 | vec4 color[16]; 530 | float mass[16]; 531 | } data; 532 | 533 | size_t count = std::min(objs.size(), size_t(16)); 534 | data.numObjects = static_cast(count); 535 | 536 | for (size_t i = 0; i < count; ++i) { 537 | data.posRadius[i] = objs[i].posRadius; 538 | data.color[i] = objs[i].color; 539 | data.mass[i] = objs[i].mass; 540 | } 541 | 542 | // Upload 543 | glBindBuffer(GL_UNIFORM_BUFFER, objectsUBO); 544 | glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(data), &data); 545 | } 546 | void uploadDiskUBO() { 547 | // disk 548 | float r1 = SagA.r_s * 2.2f; // inner radius just outside the event horizon 549 | float r2 = SagA.r_s * 5.2f; // outer radius of the disk 550 | float num = 2.0; // number of rays 551 | float thickness = 1e9f; // padding for std140 alignment 552 | float diskData[4] = { r1, r2, num, thickness }; 553 | 554 | glBindBuffer(GL_UNIFORM_BUFFER, diskUBO); 555 | glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(diskData), diskData); 556 | } 557 | 558 | vector QuadVAO(){ 559 | float quadVertices[] = { 560 | // positions // texCoords 561 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 562 | -1.0f, -1.0f, 0.0f, 0.0f, // bottom left 563 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 564 | 565 | -1.0f, 1.0f, 0.0f, 1.0f, // top left 566 | 1.0f, -1.0f, 1.0f, 0.0f, // bottom right 567 | 1.0f, 1.0f, 1.0f, 1.0f // top right 568 | }; 569 | 570 | GLuint VAO, VBO; 571 | glGenVertexArrays(1, &VAO); 572 | glGenBuffers(1, &VBO); 573 | 574 | glBindVertexArray(VAO); 575 | glBindBuffer(GL_ARRAY_BUFFER, VBO); 576 | glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW); 577 | 578 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0); 579 | glEnableVertexAttribArray(0); 580 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float))); 581 | glEnableVertexAttribArray(1); 582 | 583 | GLuint texture; 584 | glGenTextures(1, &texture); 585 | glBindTexture(GL_TEXTURE_2D, texture); 586 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 587 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 588 | glBindTexture(GL_TEXTURE_2D, texture); 589 | glTexImage2D(GL_TEXTURE_2D, 590 | 0, // mip 591 | GL_RGBA8, // internal format 592 | COMPUTE_WIDTH, 593 | COMPUTE_HEIGHT, 594 | 0, 595 | GL_RGBA, 596 | GL_UNSIGNED_BYTE, 597 | nullptr); 598 | vector VAOtexture = {VAO, texture}; 599 | return VAOtexture; 600 | } 601 | void renderScene() { 602 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 603 | glUseProgram(shaderProgram); 604 | glBindVertexArray(quadVAO); 605 | // make sure your fragment shader samples from texture unit 0: 606 | glActiveTexture(GL_TEXTURE0); 607 | glBindTexture(GL_TEXTURE_2D, texture); 608 | glDrawArrays(GL_TRIANGLES, 0, 6); 609 | glfwSwapBuffers(window); 610 | glfwPollEvents(); 611 | }; 612 | }; 613 | Engine engine; 614 | void setupCameraCallbacks(GLFWwindow* window) { 615 | glfwSetWindowUserPointer(window, &camera); 616 | 617 | glfwSetMouseButtonCallback(window, [](GLFWwindow* win, int button, int action, int mods) { 618 | Camera* cam = (Camera*)glfwGetWindowUserPointer(win); 619 | cam->processMouseButton(button, action, mods, win); 620 | }); 621 | 622 | glfwSetCursorPosCallback(window, [](GLFWwindow* win, double x, double y) { 623 | Camera* cam = (Camera*)glfwGetWindowUserPointer(win); 624 | cam->processMouseMove(x, y); 625 | }); 626 | 627 | glfwSetScrollCallback(window, [](GLFWwindow* win, double xoffset, double yoffset) { 628 | Camera* cam = (Camera*)glfwGetWindowUserPointer(win); 629 | cam->processScroll(xoffset, yoffset); 630 | }); 631 | 632 | glfwSetKeyCallback(window, [](GLFWwindow* win, int key, int scancode, int action, int mods) { 633 | Camera* cam = (Camera*)glfwGetWindowUserPointer(win); 634 | cam->processKey(key, scancode, action, mods); 635 | }); 636 | } 637 | 638 | 639 | // -- MAIN -- // 640 | int main() { 641 | setupCameraCallbacks(engine.window); 642 | vector pixels(engine.WIDTH * engine.HEIGHT * 3); 643 | 644 | auto t0 = Clock::now(); 645 | lastPrintTime = chrono::duration(t0.time_since_epoch()).count(); 646 | 647 | double lastTime = glfwGetTime(); 648 | int renderW = 800, renderH = 600, numSteps = 80000; 649 | while (!glfwWindowShouldClose(engine.window)) { 650 | glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // optional, but good practice 651 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 652 | 653 | double now = glfwGetTime(); 654 | double dt = now - lastTime; // seconds since last frame 655 | lastTime = now; 656 | 657 | // Gravity 658 | for (auto& obj : objects) { 659 | for (auto& obj2 : objects) { 660 | if (&obj == &obj2) continue; // skip self-interaction 661 | float dx = obj2.posRadius.x - obj.posRadius.x; 662 | float dy = obj2.posRadius.y - obj.posRadius.y; 663 | float dz = obj2.posRadius.z - obj.posRadius.z; 664 | float distance = sqrt(dx * dx + dy * dy + dz * dz); 665 | if (distance > 0) { 666 | vector direction = {dx / distance, dy / distance, dz / distance}; 667 | //distance *= 1000; 668 | double Gforce = (G * obj.mass * obj2.mass) / (distance * distance); 669 | 670 | double acc1 = Gforce / obj.mass; 671 | std::vector acc = {direction[0] * acc1, direction[1] * acc1, direction[2] * acc1}; 672 | if (Gravity) { 673 | obj.velocity.x += acc[0]; 674 | obj.velocity.y += acc[1]; 675 | obj.velocity.z += acc[2]; 676 | 677 | obj.posRadius.x += obj.velocity.x; 678 | obj.posRadius.y += obj.velocity.y; 679 | obj.posRadius.z += obj.velocity.z; 680 | cout << "velocity: " <