Created
May 21, 2020 17:39
-
-
Save branw/e170a5c31bf62aceb5d5255fe9648704 to your computer and use it in GitHub Desktop.
Basic C++17 ray-tracer that renders onto Notepad's multiline edit control
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Windows.h> | |
#include <iostream> | |
#include <sstream> | |
#include <tuple> | |
#include <optional> | |
#include <cmath> | |
#include <string> | |
#include <thread> | |
#include <chrono> | |
// Frames per second | |
// Noticible flickering above 25 FPS | |
double const fps = 25; | |
// Number of columns | |
double const width = 80; | |
// Number of rows | |
double const height = 40; | |
// Field of view in degrees | |
auto const fov = 40; | |
// ASCII characters representing cells from dark to light | |
// From http://paulbourke.net/dataformats/asciiart/ | |
char const shades[] = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\" ^ `'. "; | |
struct Vector3 { | |
double x, y, z; | |
Vector3(double x = 0, double y = 0, double z = 0) : x{ x }, y{ y }, z{ z } {} | |
double length() const { | |
return sqrt(x * x + y * y + z * z); | |
} | |
void normalize() { | |
auto len = length(); | |
x /= len; | |
y /= len; | |
z /= len; | |
} | |
double dot(Vector3 other) const { | |
return x * other.x + y * other.y + z * other.z; | |
} | |
Vector3 cross(Vector3 other) const { | |
return { | |
y * other.z - z * other.y, | |
z * other.x - x * other.z, | |
x * other.y - y * other.x | |
}; | |
} | |
Vector3 operator+(Vector3 other) const { | |
return { x + other.x, y + other.y, z + other.z }; | |
} | |
Vector3 operator-(Vector3 other) const { | |
return { x - other.x, y - other.y, z - other.z }; | |
} | |
Vector3 operator*(double other) const { | |
return { x * other, y * other, z * other }; | |
} | |
}; | |
struct Ray { | |
Vector3 position; | |
Vector3 direction; | |
Ray(Vector3 position, Vector3 direction) : position{ position }, direction{ direction } { | |
this->direction.normalize(); | |
} | |
}; | |
struct Sphere { | |
Vector3 position; | |
double radius; | |
std::optional<Ray> intersect(Ray ray) const { | |
// Create a cross-section of the sphere co-planar to the ray | |
auto v = position - ray.position; | |
auto b = v.dot(ray.direction); | |
auto d2 = b * b - v.dot(v) + radius * radius; | |
// No intersection | |
if (d2 < 0) return {}; | |
// Two possible solutions to the quadratic: pick closest | |
auto d = sqrt(d2); | |
auto t0 = b - d, t1 = b + d; | |
auto t = (t0 > 0 ? t0 : t1); | |
auto intersection_pos = ray.position + ray.direction * t; | |
return Ray{ intersection_pos, intersection_pos - position }; | |
} | |
}; | |
auto const pi = atan(1) * 4; | |
auto const fov_factor = tan((fov * (pi / 180)) / 2); | |
auto const aspect_ratio = (width / height) / 2; | |
int main() { | |
// Launch Notepad | |
char cmd[256] = { 0 }; | |
snprintf(cmd, sizeof(cmd), "notepad"); | |
PROCESS_INFORMATION proc_info = { 0 }; | |
STARTUPINFOA startup_info = { 0 }; | |
startup_info.cb = sizeof(STARTUPINFO); | |
if (!CreateProcessA(nullptr, cmd, NULL, NULL, TRUE, NULL, NULL, NULL, | |
&startup_info, &proc_info)) { | |
return 1; | |
} | |
CloseHandle(proc_info.hThread); | |
auto desired_pid = GetProcessId(proc_info.hProcess); | |
// Find the Notepad edit control | |
HWND window_handle = nullptr, edit_control_handle = nullptr; | |
for (;;) { | |
auto tuple = std::make_tuple(desired_pid, &window_handle, &edit_control_handle); | |
if (!EnumWindows([](HWND hWnd, LPARAM lParam) -> BOOL { | |
auto [desired_pid, hWndParent, hWndEdit] = *reinterpret_cast<decltype(&tuple)>(lParam); | |
// Skip other processes | |
DWORD pid = 0; | |
GetWindowThreadProcessId(hWnd, &pid); | |
if (pid != desired_pid) return TRUE; | |
// Try to find the "Edit" control | |
*hWndEdit = FindWindowExA(hWnd, NULL, "Edit", NULL); | |
if (*hWndEdit == NULL) return TRUE; | |
// Save the parent's handle so we can resize later | |
*hWndParent = hWnd; | |
return FALSE; | |
}, reinterpret_cast<LPARAM>(&tuple))) break; | |
} | |
Vector3 eye_pos{ 5, 0, 0 }; | |
auto time = 0.f; | |
auto time_step = 1 / fps; | |
std::string buffer; | |
for (;;) { | |
// Check if Notepad was closed | |
DWORD exit_code = 0; | |
if (!GetExitCodeProcess(proc_info.hProcess, &exit_code) || exit_code != STILL_ACTIVE) { | |
return 0; | |
} | |
// Move the light source around | |
auto light_pos = Vector3{ | |
10,//cos(time * pi * 2) * 10, | |
cos(time * pi * 3) * 10, | |
5 };//sin(time * pi * 2) * 10 }; | |
// Pan the ball | |
Sphere ball{ Vector3{ 0, 10 * cos(time * pi), 5 }, 1 }; | |
// Calculate eye angles | |
auto eye_dir = ball.position - eye_pos; | |
eye_dir.normalize(); | |
auto eye_right = eye_dir.cross(Vector3{ 0, -1, 0 }); | |
eye_right.normalize(); | |
auto eye_up = eye_right.cross(eye_dir); | |
eye_up.normalize(); | |
// Ray trace | |
for (auto y = 0; y < height; y++) { | |
for (auto x = 0; x < width; x++) { | |
auto look_pos = eye_pos + eye_dir + | |
eye_right * fov_factor * aspect_ratio * (x / width - 0.5) + | |
eye_up * fov_factor * (y / height - 0.5); | |
Ray test_ray{ eye_pos, look_pos - eye_pos }; | |
if (auto intersection = ball.intersect(test_ray)) { | |
auto light_dir = light_pos - intersection->position; | |
light_dir.normalize(); | |
double diffuse = intersection->direction.dot(light_dir); | |
if (diffuse <= 0) { | |
diffuse = 0; | |
} | |
int shade_index = (diffuse * diffuse * diffuse) * (sizeof(shades) - 1); | |
buffer += shades[shade_index]; | |
} | |
else { | |
buffer += ' '; | |
} | |
} | |
buffer += '\n'; | |
} | |
// Write the buffer to Notepad | |
SendMessageA(edit_control_handle, WM_SETTEXT, | |
buffer.length(), reinterpret_cast<LPARAM>(buffer.c_str())); | |
buffer.clear(); | |
time += time_step; | |
std::this_thread::sleep_for(std::chrono::milliseconds(static_cast<int>(time_step * 1000))); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment