diff --git a/Programming/TRAK/.gitignore b/Programming/TRAK/.gitignore new file mode 100644 index 00000000..126fa691 --- /dev/null +++ b/Programming/TRAK/.gitignore @@ -0,0 +1,166 @@ +.vscode/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +*.exr \ No newline at end of file diff --git a/Programming/TRAK/.idea/.gitignore b/Programming/TRAK/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/Programming/TRAK/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Programming/TRAK/.idea/TRAK.iml b/Programming/TRAK/.idea/TRAK.iml new file mode 100644 index 00000000..8dedcfa7 --- /dev/null +++ b/Programming/TRAK/.idea/TRAK.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Programming/TRAK/.idea/inspectionProfiles/profiles_settings.xml b/Programming/TRAK/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/Programming/TRAK/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/Programming/TRAK/.idea/misc.xml b/Programming/TRAK/.idea/misc.xml new file mode 100644 index 00000000..8f7d572f --- /dev/null +++ b/Programming/TRAK/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/Programming/TRAK/.idea/modules.xml b/Programming/TRAK/.idea/modules.xml new file mode 100644 index 00000000..2c5ef4e6 --- /dev/null +++ b/Programming/TRAK/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Programming/TRAK/.idea/vcs.xml b/Programming/TRAK/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/Programming/TRAK/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Programming/TRAK/.python-version b/Programming/TRAK/.python-version new file mode 100644 index 00000000..2d4715b6 --- /dev/null +++ b/Programming/TRAK/.python-version @@ -0,0 +1 @@ +3.11.11 diff --git a/Programming/TRAK/.vscode/extensions.json b/Programming/TRAK/.vscode/extensions.json new file mode 100644 index 00000000..88c93d43 --- /dev/null +++ b/Programming/TRAK/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "ms-python.black-formatter", + "ms-python.autopep8", + "ms-python.pylint" + ] +} \ No newline at end of file diff --git a/Programming/TRAK/README.md b/Programming/TRAK/README.md new file mode 100644 index 00000000..0552da05 --- /dev/null +++ b/Programming/TRAK/README.md @@ -0,0 +1,23 @@ +## Wywoływanie z terminala + +```bash +# Wywołanie algorytmu ray tracing z domyślnymi parametrami i sceną +python main.py --algorithm ray_tracing +``` + +```bash +# Wywołanie algorytmu ray tracing ze specyfikacją sceny z folderu scenes, liczbą sampli na pixek, rozdzielczością, środowiskiem i rozmyciem środowiska +python main.py --scene three_spheres --samples_per_pixel 100 --resolution 100x100 --environment lake.png --env_blur 10 +``` + +```bash +# Wywołanie algorytmu *photon mapping* ze specyfikacją liczby fotonów i maksymalnej głębokości +python main.py --algorithm photon_mapping --max_depth 4 --num_photons 1000 +``` + +```bash +# Wywołanie algorytmu *photon mapping* wykonanego w c++ +cd photonmappnig/cpp +./compile.sh +./photon_mapping +``` \ No newline at end of file diff --git a/Programming/TRAK/code/mapa_srodowiska/README.md b/Programming/TRAK/code/mapa_srodowiska/README.md new file mode 100644 index 00000000..a0efbc03 --- /dev/null +++ b/Programming/TRAK/code/mapa_srodowiska/README.md @@ -0,0 +1,2 @@ +Download file in (!) EXR (!) format +https://polyhaven.com/a/lilienstein \ No newline at end of file diff --git a/Programming/TRAK/code/mapa_srodowiska/lilienstein_1k.exr b/Programming/TRAK/code/mapa_srodowiska/lilienstein_1k.exr new file mode 100644 index 00000000..ebcdb200 Binary files /dev/null and b/Programming/TRAK/code/mapa_srodowiska/lilienstein_1k.exr differ diff --git a/Programming/TRAK/code/mapa_srodowiska/main.py b/Programming/TRAK/code/mapa_srodowiska/main.py new file mode 100644 index 00000000..311581f8 --- /dev/null +++ b/Programming/TRAK/code/mapa_srodowiska/main.py @@ -0,0 +1,162 @@ +import OpenEXR +import Imath +import os +import OpenGL.GL as gl +import OpenGL.GLUT as glut +import OpenGL.GLU as glu +import math + +# Camera parameters +camera_angle_x = 0.0 +camera_angle_y = 0.0 +camera_distance = 2.0 +mouse_last_x = 0 +mouse_last_y = 0 +mouse_left_down = False + +def load_hdr_environment_map(filepath): + """ + Load an HDR environment map from an OpenEXR file. + + Args: + filepath (str): Path to the HDR file. + + Returns: + tuple: A tuple containing the width, height, and a bytes object representing the HDR environment map in RGB format. + """ + if not os.path.exists(filepath): + raise FileNotFoundError(f"File not found: {filepath}") + + # Check file permissions + if not os.access(filepath, os.R_OK): + raise PermissionError(f"File is not readable: {filepath}") + + # Open the EXR file + try: + exr_file = OpenEXR.InputFile(filepath) + except Exception as e: + raise OSError(f"Unable to open '{filepath}' for read: {str(e)}") + + # Get the image dimensions + header = exr_file.header() + dw = header['dataWindow'] + width = dw.max.x - dw.min.x + 1 + height = dw.max.y - dw.min.y + 1 + + # Define the channel names (R, G, B) + channels = ['R', 'G', 'B'] + + # Read the channel data + channel_data = { + channel: exr_file.channel(channel, Imath.PixelType(Imath.PixelType.FLOAT)) + for channel in channels + } + + # Combine channel data into a single bytes object + hdr_image = bytearray(width * height * 3 * 4) # 3 channels, 4 bytes per float + for i, channel in enumerate(channels): + channel_buffer = channel_data[channel] + for j in range(height): + for k in range(width): + index = (j * width + k) * 3 * 4 + i * 4 + hdr_image[index:index + 4] = channel_buffer[(j * width + k) * 4:(j * width + k + 1) * 4] + + # Flip the image vertically + flipped_hdr_image = bytearray(width * height * 3 * 4) + row_size = width * 3 * 4 + for j in range(height): + src_index = j * row_size + dst_index = (height - 1 - j) * row_size + flipped_hdr_image[dst_index:dst_index + row_size] = hdr_image[src_index:src_index + row_size] + + return width, height, bytes(flipped_hdr_image) + +def display_hdr_image(width, height, hdr_image): + gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) + + # Enable texture mapping + gl.glEnable(gl.GL_TEXTURE_2D) + texture_id = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id) + + # Set texture parameters + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + + # Load the texture + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB32F, width, height, 0, gl.GL_RGB, gl.GL_FLOAT, hdr_image) + + # Set up the viewport and projection + gl.glViewport(0, 0, glut.glutGet(glut.GLUT_WINDOW_WIDTH), glut.glutGet(glut.GLUT_WINDOW_HEIGHT)) + gl.glMatrixMode(gl.GL_PROJECTION) + gl.glLoadIdentity() + glu.gluPerspective(45.0, glut.glutGet(glut.GLUT_WINDOW_WIDTH) / float(glut.glutGet(glut.GLUT_WINDOW_HEIGHT)), 0.1, 100.0) + gl.glMatrixMode(gl.GL_MODELVIEW) + gl.glLoadIdentity() + + # Apply camera transformations + gl.glTranslatef(0.0, 0.0, -camera_distance) + gl.glRotatef(camera_angle_y, 1.0, 0.0, 0.0) + gl.glRotatef(camera_angle_x, 0.0, 1.0, 0.0) + + # Draw a textured sphere to simulate being inside the HDR environment + quadric = glu.gluNewQuadric() + glu.gluQuadricTexture(quadric, gl.GL_TRUE) + glu.gluSphere(quadric, 50.0, 50, 50) + glu.gluDeleteQuadric(quadric) + + # Disable texture mapping + gl.glDisable(gl.GL_TEXTURE_2D) + + glut.glutSwapBuffers() + +def mouse_motion(x, y): + global mouse_last_x, mouse_last_y, camera_angle_x, camera_angle_y + if mouse_left_down: + dx = x - mouse_last_x + dy = y - mouse_last_y + camera_angle_x += dx * 0.1 + camera_angle_y += dy * 0.1 + mouse_last_x = x + mouse_last_y = y + glut.glutPostRedisplay() + +def mouse_button(button, state, x, y): + global mouse_left_down + if button == glut.GLUT_LEFT_BUTTON: + if state == glut.GLUT_DOWN: + mouse_left_down = True + mouse_last_x = x + mouse_last_y = y + elif state == glut.GLUT_UP: + mouse_left_down = False + +def main(filepath): + try: + width, height, hdr_map = load_hdr_environment_map(filepath) + print("HDR Map Loaded. Dimensions:", width, "x", height) + + # Initialize GLUT and create window + glut.glutInit() + glut.glutInitDisplayMode(glut.GLUT_DOUBLE | glut.GLUT_RGB | glut.GLUT_DEPTH) + glut.glutInitWindowSize(800, 600) # Set initial window size + glut.glutCreateWindow(b"HDR Environment Map") + + # Set display callback + glut.glutDisplayFunc(lambda: display_hdr_image(width, height, hdr_map)) + + # Set mouse callbacks + glut.glutMotionFunc(mouse_motion) + glut.glutMouseFunc(mouse_button) + + # Start the GLUT main loop + glut.glutMainLoop() + except Exception as e: + print(e) + +# Example usage +if __name__ == "__main__": + filepath = "lilienstein_4k.exr" + main(filepath) diff --git a/Programming/TRAK/code/photonmapping/cpp/.gitignore b/Programming/TRAK/code/photonmapping/cpp/.gitignore new file mode 100644 index 00000000..d6a6547e --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/.gitignore @@ -0,0 +1,3 @@ +photon_mapping +*.ppm +*.jpg \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/cpp/Plane.h b/Programming/TRAK/code/photonmapping/cpp/Plane.h new file mode 100644 index 00000000..75b789bb --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/Plane.h @@ -0,0 +1,27 @@ +#ifndef PLANE_H + +class Plane { +public: + Vector3 point; // A point on the plane + Vector3 normal; // Normal to the plane + Vector3 color; + + Plane(const Vector3& p, const Vector3& n, const Vector3& col) + : point(p), normal(n.normalize()), color(col) {} + + bool intersect(const Vector3& ray_origin, const Vector3& ray_direction, double& t, Vector3& hit_point, Vector3& hit_normal) const { + double denom = normal.dot(ray_direction); + if (std::abs(denom) > 1e-6) { + t = (point - ray_origin).dot(normal) / denom; + if (t >= 1e-4) { + hit_point = ray_origin + ray_direction * t; + hit_normal = normal; + return true; + } + } + return false; + } +}; + + +#endif \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/cpp/Sphere.h b/Programming/TRAK/code/photonmapping/cpp/Sphere.h new file mode 100644 index 00000000..38c2d223 --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/Sphere.h @@ -0,0 +1,31 @@ +#ifndef SPHERE_H +#define SPHERE_H + +#include "Vector3.h" + +class Sphere { +public: + Vector3 center; + double radius; + Vector3 color; + + Sphere(const Vector3& c, double r, const Vector3& col) + : center(c), radius(r), color(col) {} + + bool intersect(const Vector3& ray_origin, const Vector3& ray_direction, double& t, Vector3& hit_point, Vector3& normal) const { + Vector3 oc = ray_origin - center; + double a = ray_direction.dot(ray_direction); + double b = 2.0 * oc.dot(ray_direction); + double c = oc.dot(oc) - radius * radius; + double discriminant = b * b - 4 * a * c; + if (discriminant > 0) { + t = (-b - std::sqrt(discriminant)) / (2.0 * a); + hit_point = ray_origin + ray_direction * t; + normal = (hit_point - center).normalize(); + return true; + } + return false; + } +}; + +#endif // SPHERE_H \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/cpp/Vector3.h b/Programming/TRAK/code/photonmapping/cpp/Vector3.h new file mode 100644 index 00000000..cf19b746 --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/Vector3.h @@ -0,0 +1,23 @@ +#ifndef VECTOR3_H +#define VECTOR3_H + +#include + +class Vector3 { +public: + double x, y, z; + + Vector3(double x_=0, double y_=0, double z_=0): x(x_), y(y_), z(z_) {} + + Vector3 operator + (const Vector3& v) const { return Vector3(x+v.x, y+v.y, z+v.z); } + Vector3 operator - (const Vector3& v) const { return Vector3(x-v.x, y-v.y, z-v.z); } + Vector3 operator * (double scalar) const { return Vector3(x*scalar, y*scalar, z*scalar); } + Vector3 operator / (double scalar) const { return Vector3(x/scalar, y/scalar, z/scalar); } + Vector3 operator - () const { return Vector3(-x, -y, -z); } + Vector3 operator * (const Vector3& v) const { return Vector3(x*v.x, y*v.y, z*v.z); } + double dot(const Vector3& v) const { return x*v.x + y*v.y + z*v.z; } + double norm() const { return std::sqrt(x*x + y*y + z*z); } + Vector3 normalize() const { double n = norm(); return Vector3(x/n, y/n, z/n); } +}; + +#endif // VECTOR3_H \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/cpp/compile.sh b/Programming/TRAK/code/photonmapping/cpp/compile.sh new file mode 100755 index 00000000..19b163a4 --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/compile.sh @@ -0,0 +1 @@ +g++ -O2 main.cpp -o photon_mapping \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/cpp/main.cpp b/Programming/TRAK/code/photonmapping/cpp/main.cpp new file mode 100644 index 00000000..64c6ad37 --- /dev/null +++ b/Programming/TRAK/code/photonmapping/cpp/main.cpp @@ -0,0 +1,296 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "Vector3.h" +#include "Sphere.h" +#include "Plane.h" + + +// Define the photon +struct Photon { + Vector3 position; + Vector3 direction; + Vector3 power; + + Photon(const Vector3& pos, const Vector3& dir, const Vector3& pow) + : position(pos), direction(dir), power(pow) {} +}; + +// Define a simple plane + +// Random number generators +std::mt19937 rng; +std::uniform_real_distribution uni_dist(0.0, 1.0); + +// Random unit vector in a sphere +Vector3 random_unit_vector() { + double theta = 2 * M_PI * uni_dist(rng); + double z = 2 * uni_dist(rng) - 1; + double r = std::sqrt(1 - z*z); + return Vector3(r * std::cos(theta), r * std::sin(theta), z); +} + +// Random direction in hemisphere around normal +Vector3 random_hemisphere_direction(const Vector3& normal) { + Vector3 dir = random_unit_vector(); + if (dir.dot(normal) < 0) { + dir = dir * -1; + } + return dir; +} + +// Global variables +std::vector photon_map; +std::vector objects; // Pointers to objects +std::vector object_types; // 0 for Sphere, 1 for Plane +int ray_number = 0; +int photon_number = 0; + + +// Scene setup +Sphere sphere(Vector3(0, 0, -5), 1.0, Vector3(1, 0, 0)); // Red sphere +Plane plane(Vector3(0, -1, 0), Vector3(0, 1, 0), Vector3(0.5, 0.5, 0.5)); // Gray plane +// Light source +Vector3 light_position(-5, 5, -5); +Vector3 light_power(1000.0, 1000.0, 1000.0); // Intense white light, will scale in code + + +// Functions +void emit_photons(const int num_photons, const int max_depth); +void trace_photon(Photon photon, int depth, const int max_depth); +Vector3 trace_ray(const Vector3& ray_origin, const Vector3& ray_direction, const double gather_radius); +Vector3 compute_direct_light(const Vector3& point, const Vector3& normal); +Vector3 estimate_radiance(const Vector3& point, const Vector3& normal, const double gather_radius); + +// Main execution +int main(int argc, char* argv[]) { + auto start = std::chrono::high_resolution_clock::now(); // Start timer + int num_photons = 10000; + int max_depth = 5; + double gather_radius = 0.5; + + if (argc > 1) { + num_photons = std::atoi(argv[1]); + if (num_photons <= 0) { + std::cerr << "Invalid number of photons. Using default: 10000" << std::endl; + num_photons = 10000; + } + } + if (argc > 2) { + max_depth = std::atoi(argv[2]); + if (max_depth < 0) { + std::cerr << "Invalid max_depth. Using default: 5" << std::endl; + max_depth = 5; + } + } + if (argc > 3) { + gather_radius = std::atof(argv[3]); + if (gather_radius <= 0) { + std::cerr << "Invalid gather_radius. Using default: 0.5" << std::endl; + gather_radius = 0.5; + } + } + std::cout << "Number of photons: " << num_photons << std::endl; + std::cout << "Max depth: " << max_depth << std::endl; + std::cout << "Gather radius: " << gather_radius << std::endl; + + // Seed random number generator + rng.seed(std::random_device()()); + + std::cout << "Emitting photons..." << std::endl; + // Add objects to the scene + objects.push_back(&sphere); object_types.push_back(0); + objects.push_back(&plane); object_types.push_back(1); + + emit_photons(num_photons, max_depth); + const int photons_in_map = photon_map.size(); + + std::cout << "Rendering image..." << std::endl; + int width = 200; + int height = 100; + std::vector image(width * height); + + double aspect_ratio = double(width) / height; + double fov = M_PI / 3.0; // 60 degrees field of view + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + // Convert pixel coordinate to camera ray + double px = (2 * (x + 0.5) / double(width) - 1) * tan(fov / 2.0) * aspect_ratio; + double py = (1 - 2 * (y + 0.5) / double(height)) * tan(fov / 2.0); + Vector3 ray_origin(0, 0, 0); + Vector3 ray_direction = Vector3(px, py, -1).normalize(); + Vector3 color = trace_ray(ray_origin, ray_direction, gather_radius); + image[y * width + x] = Vector3( + std::min(color.x, 1.0), + std::min(color.y, 1.0), + std::min(color.z, 1.0) + ); + } + } + + // Save image to PPM file + std::ofstream ofs("render.ppm"); + ofs << "P3\n" << width << " " << height << "\n255\n"; + double gamma = 1.0 / 2.2; // Corrected gamma division + for (const auto& pixel : image) { + int r = static_cast(std::pow(pixel.x, gamma) * 255); + int g = static_cast(std::pow(pixel.y, gamma) * 255); + int b = static_cast(std::pow(pixel.z, gamma) * 255); + // Clamp values between 0 and 255 + r = std::max(0, std::min(255, r)); + g = std::max(0, std::min(255, g)); + b = std::max(0, std::min(255, b)); + ofs << r << " " << g << " " << b << "\n"; + } + ofs.close(); + std::cout << "Image generated" << std::endl; + std::cout << "width: " << width << std::endl; + std::cout << "height: " << width << std::endl; + std::cout << "photons in map: " << photons_in_map << std::endl; + std::cout << "count of photons: " << photon_number << std::endl; + std::cout << "count of rays: " << ray_number << std::endl; + std::cout << "Image saved to render.ppm" << std::endl; + std::chrono::duration elapsed = std::chrono::high_resolution_clock::now() - start; + std::cout << "Execution time: " << elapsed.count() << " seconds" << std::endl; + return 0; +} + +void emit_photons(const int num_photons, const int max_depth) { + Vector3 per_photon_power = light_power / num_photons; // Scale light power per photon + for (int i = 0; i < num_photons; ++i) { + Vector3 direction = random_unit_vector(); + Photon photon(light_position, direction, per_photon_power); + trace_photon(photon, 0, max_depth); + } +} + +void trace_photon(Photon photon, int depth, const int max_depth) { + photon_number++; + if (depth > max_depth) { + return; + } + double closest_t = std::numeric_limits::infinity(); + void* hit_object = nullptr; + int hit_type = -1; + Vector3 hit_point, normal; + // Find the nearest intersection + for (size_t i = 0; i < objects.size(); ++i) { + double t; + Vector3 temp_hit_point, temp_normal; + bool hit = false; + if (object_types[i] == 0) { + Sphere* obj = static_cast(objects[i]); + hit = obj->intersect(photon.position, photon.direction, t, temp_hit_point, temp_normal); + } else if (object_types[i] == 1) { + Plane* obj = static_cast(objects[i]); + hit = obj->intersect(photon.position, photon.direction, t, temp_hit_point, temp_normal); + } + if (hit && t < closest_t) { + closest_t = t; + hit_object = objects[i]; + hit_type = object_types[i]; + hit_point = temp_hit_point; + normal = temp_normal; + } + } + if (hit_object) { + photon_map.push_back(Photon(hit_point, photon.direction, photon.power)); + // Diffuse reflection + Vector3 new_direction = random_hemisphere_direction(normal); + photon.position = hit_point; + photon.direction = new_direction; + // Absorb some power + photon.power = photon.power * 0.8; // Simple absorption + trace_photon(photon, depth + 1, max_depth); + } +} + +Vector3 trace_ray(const Vector3& ray_origin, const Vector3& ray_direction, const double gather_radius) { + ray_number++; + double closest_t = std::numeric_limits::infinity(); + void* hit_object = nullptr; + int hit_type = -1; + Vector3 hit_point, normal; + Vector3 obj_color; + // Find the nearest intersection + for (size_t i = 0; i < objects.size(); ++i) { + double t; + Vector3 temp_hit_point, temp_normal; + bool hit = false; + if (object_types[i] == 0) { + Sphere* obj = static_cast(objects[i]); + hit = obj->intersect(ray_origin, ray_direction, t, temp_hit_point, temp_normal); + if (hit) obj_color = obj->color; + } else if (object_types[i] == 1) { + Plane* obj = static_cast(objects[i]); + hit = obj->intersect(ray_origin, ray_direction, t, temp_hit_point, temp_normal); + if (hit) obj_color = obj->color; + } + if (hit && t < closest_t) { + closest_t = t; + hit_object = objects[i]; + hit_type = object_types[i]; + hit_point = temp_hit_point; + normal = temp_normal; + } + } + if (hit_object) { + Vector3 direct_light = compute_direct_light(hit_point, normal); + Vector3 indirect_light = estimate_radiance(hit_point, normal, gather_radius); + return obj_color * (direct_light + indirect_light); // Component-wise multiplication + } else { + return Vector3(0, 0, 0); // Background color + } +} + +Vector3 compute_direct_light(const Vector3& point, const Vector3& normal) { + // Simple Lambertian reflection from light source + Vector3 direction_to_light = (light_position - point).normalize(); + // Shadow ray + Vector3 shadow_origin = point + normal * 1e-5; + Vector3 shadow_ray = direction_to_light; + bool in_shadow = false; + for (size_t i = 0; i < objects.size(); ++i) { + double t; + Vector3 temp_hit_point, temp_normal; + bool hit = false; + if (object_types[i] == 0) { + Sphere* obj = static_cast(objects[i]); + hit = obj->intersect(shadow_origin, shadow_ray, t, temp_hit_point, temp_normal); + } else if (object_types[i] == 1) { + Plane* obj = static_cast(objects[i]); + hit = obj->intersect(shadow_origin, shadow_ray, t, temp_hit_point, temp_normal); + } + if (hit) { + in_shadow = true; + break; + } + } + if (in_shadow) { + return Vector3(0, 0, 0); + } else { + double intensity = std::max(0.0, normal.dot(direction_to_light)); + double distance2 = (light_position - point).dot(light_position - point); + return (light_power * intensity) / (4 * M_PI * distance2); + } +} + +Vector3 estimate_radiance(const Vector3& point, const Vector3& normal, const double gather_radius) { + // Gather photons within the gather_radius + Vector3 accumulated_power(0.0, 0.0, 0.0); + for (const auto& photon : photon_map) { + double distance = (photon.position - point).norm(); + if (distance < gather_radius) { + double weight = std::max(0.0, normal.dot((-photon.direction).normalize())); + accumulated_power = accumulated_power + photon.power * weight; + } + } + double area = M_PI * gather_radius * gather_radius; + return accumulated_power / area; // Removed division by num_photons +} \ No newline at end of file diff --git a/Programming/TRAK/code/photonmapping/python/main.py b/Programming/TRAK/code/photonmapping/python/main.py new file mode 100644 index 00000000..92567914 --- /dev/null +++ b/Programming/TRAK/code/photonmapping/python/main.py @@ -0,0 +1,235 @@ +import numpy as np +import matplotlib.pyplot as plt +import time + +PHOTONS = 0 + +# Define basic vector operations +class Vector3: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __add__(self, other): + return Vector3(self.x+other.x, self.y+other.y, self.z+other.z) + + def __sub__(self, other): + return Vector3(self.x-other.x, self.y-other.y, self.z-other.z) + + def __mul__(self, scalar): + return Vector3(self.x*scalar, self.y*scalar, self.z*scalar) + + def dot(self, other): + return self.x*other.x + self.y*other.y + self.z*other.z + + def norm(self): + return np.sqrt(self.dot(self)) + + def normalize(self): + n = self.norm() + return Vector3(self.x/n, self.y/n, self.z/n) + +# Define the photon +class Photon: + def __init__(self, position, direction, power): + self.position = position + self.direction = direction + self.power = power + +# Define a simple sphere +class Sphere: + def __init__(self, center, radius, color): + self.center = center + self.radius = radius + self.color = color + + def intersect(self, ray_origin, ray_direction): + # Solve quadratic equation for intersection + oc = ray_origin - self.center + a = ray_direction.dot(ray_direction) + b = 2.0 * oc.dot(ray_direction) + c = oc.dot(oc) - self.radius*self.radius + discriminant = b*b - 4*a*c + if discriminant < 0: + return None # No intersection + else: + t = (-b - np.sqrt(discriminant)) / (2.0*a) + if t < 0: + t = (-b + np.sqrt(discriminant)) / (2.0*a) + if t < 0: + return None + hit_point = ray_origin + ray_direction * t + normal = (hit_point - self.center).normalize() + return (t, hit_point, normal) + +# Define a simple plane +class Plane: + def __init__(self, point, normal, color): + self.point = point + self.normal = normal.normalize() + self.color = color + + def intersect(self, ray_origin, ray_direction): + denom = self.normal.dot(ray_direction) + if abs(denom) > 1e-6: + t = (self.point - ray_origin).dot(self.normal) / denom + if t >= 0: + hit_point = ray_origin + ray_direction * t + return (t, hit_point, self.normal) + return None + +# Scene setup +sphere = Sphere(Vector3(0, 0, -5), 1.0, np.array([1, 0, 0])) # Red sphere +plane = Plane(Vector3(0, -1, 0), Vector3(0, 1, 0), np.array([0.5, 0.5, 0.5])) # Gray plane + +objects = [sphere, plane] + +# Light source +light_position = Vector3(-5, 5, -5) +light_power = np.array([1, 1, 1]) * 1000 # Intense white light + +# Photon map +photon_map = [] + +# Parameters +num_photons = 10000 # Number of photons to emit +max_depth = 5 # Maximum number of bounces +gather_radius = 0.5 # Radius for radiance estimation + +def emit_photons(): + for _ in range(num_photons): + # Emit photons in random directions from the light source + direction = random_unit_vector() + power = light_power / num_photons + photon = Photon(light_position, direction, power) + trace_photon(photon, 0) + +def trace_photon(photon, depth): + global PHOTONS + PHOTONS += 1 + if depth > max_depth: + return + closest_t = np.inf + hit_object = None + hit_info = None + # Find the nearest intersection + for obj in objects: + result = obj.intersect(photon.position, photon.direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal) + if hit_object: + hit_point, normal = hit_info + photon_map.append((hit_point, photon.power)) + # Diffuse reflection + new_direction = random_hemisphere_direction(normal) + photon.position = hit_point + photon.direction = new_direction + # Absorb some power + photon.power = photon.power * 0.8 # Simple absorption + trace_photon(photon, depth+1) + +def random_unit_vector(): + theta = np.random.uniform(0, 2*np.pi) + z = np.random.uniform(-1, 1) + r = np.sqrt(1 - z*z) + return Vector3(r * np.cos(theta), r * np.sin(theta), z) + +def random_hemisphere_direction(normal): + dir = random_unit_vector() + if dir.dot(normal) < 0: + dir = Vector3(-dir.x, -dir.y, -dir.z) + return dir + +def render_image(width, height): + aspect_ratio = width / height + fov = np.pi / 3 # 60 degrees field of view + image = np.zeros((height, width, 3)) + for y in range(height): + for x in range(width): + # Convert pixel coordinate to camera ray + px = (2 * (x + 0.5) / width - 1) * np.tan(fov / 2) * aspect_ratio + py = (1 - 2 * (y + 0.5) / height) * np.tan(fov / 2) + ray_origin = Vector3(0, 0, 0) + ray_direction = Vector3(px, py, -1).normalize() + color = trace_ray(ray_origin, ray_direction) + image[y, x, :] = np.clip(color, 0, 1) + return image + +def trace_ray(ray_origin, ray_direction): + closest_t = np.inf + hit_object = None + hit_info = None + # Find the nearest intersection + for obj in objects: + result = obj.intersect(ray_origin, ray_direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal, obj.color) + if hit_object: + hit_point, normal, color = hit_info + direct_light = compute_direct_light(hit_point, normal) + indirect_light = estimate_radiance(hit_point, normal) + return color * (direct_light + indirect_light) + else: + return np.array([0, 0, 0]) # Background color + +def compute_direct_light(point, normal): + # Simple Lambertian reflection from light source + direction_to_light = (light_position - point).normalize() + # Shadow ray + shadow_origin = point + normal * 1e-5 + shadow_ray = direction_to_light + in_shadow = False + for obj in objects: + result = obj.intersect(shadow_origin, shadow_ray) + if result: + in_shadow = True + break + if in_shadow: + return np.array([0, 0, 0]) + else: + intensity = max(0, normal.dot(direction_to_light)) + return intensity * light_power / (4 * np.pi * (light_position - point).norm()**2) + +def estimate_radiance(point, normal): + # Gather photons within the gather_radius + accumulated_power = np.array([0.0, 0.0, 0.0]) + for photon_pos, photon_power in photon_map: + distance = (photon_pos - point).norm() + if distance < gather_radius: + weight = max(0, normal.dot((photon_pos - point).normalize())) + accumulated_power += photon_power * weight + area = np.pi * gather_radius ** 2 + return accumulated_power / (area * num_photons) + +# Main execution +if __name__ == '__main__': + print("Emitting photons...") + emit_photons() + + print("Rendering image...") + width = 100 + height = 100 + t0 = time.time() + image = render_image(width, height) + + print (f"Render Took: {round(time.time() - t0, 2)}s\n" + f"resolution: {width}x{height}\n" + f"photons (emitted): {num_photons}\n" + f"photons (reflected): {PHOTONS - num_photons}\n" + f"photons (total): {PHOTONS}\n" + f"rays: {width*height}" + ) + + # Display the image + plt.imshow(image) + plt.axis('off') + plt.show() diff --git a/Programming/TRAK/config.ini b/Programming/TRAK/config.ini new file mode 100644 index 00000000..987bfe7b --- /dev/null +++ b/Programming/TRAK/config.ini @@ -0,0 +1,16 @@ +[DEFAULT] +algorithm = ray_tracing +scene = cornell_box +environment = lake.png +env_blur = 0 +resolution = 400x300 +output = output.png + +[ray_tracing] +max_depth = 5 +samples_per_pixel = 6 + +[photon_mapping] +num_photons = 10000 +max_depth = 5 +gather_radius = 0.5 \ No newline at end of file diff --git a/Programming/TRAK/docs/TRAK___Projekty.pdf b/Programming/TRAK/docs/TRAK___Projekty.pdf new file mode 100644 index 00000000..f6aaf826 Binary files /dev/null and b/Programming/TRAK/docs/TRAK___Projekty.pdf differ diff --git a/Programming/TRAK/environments/README.md b/Programming/TRAK/environments/README.md new file mode 100644 index 00000000..913b13a2 --- /dev/null +++ b/Programming/TRAK/environments/README.md @@ -0,0 +1,3 @@ +# Environment directory + +Environments with .hdr or .hdri extension should stored in this directory. These files are used to create the skybox in the scene. \ No newline at end of file diff --git a/Programming/TRAK/environments/lake.png b/Programming/TRAK/environments/lake.png new file mode 100644 index 00000000..f7541a2c Binary files /dev/null and b/Programming/TRAK/environments/lake.png differ diff --git a/Programming/TRAK/environments/miramar.jpeg b/Programming/TRAK/environments/miramar.jpeg new file mode 100644 index 00000000..1a720c20 Binary files /dev/null and b/Programming/TRAK/environments/miramar.jpeg differ diff --git a/Programming/TRAK/environments/stormydays.png b/Programming/TRAK/environments/stormydays.png new file mode 100644 index 00000000..0e76d1a1 Binary files /dev/null and b/Programming/TRAK/environments/stormydays.png differ diff --git a/Programming/TRAK/main.py b/Programming/TRAK/main.py new file mode 100644 index 00000000..53d8a8db --- /dev/null +++ b/Programming/TRAK/main.py @@ -0,0 +1,76 @@ +import argparse +from rendering import ray_trace +from utils import load_config, parse_resolution +import importlib +import os +import matplotlib.pyplot as plt +from photon_mapping import render_photon_mapping + +def main(): + # default config + config = load_config('config.ini') + + # Parse + parser = argparse.ArgumentParser(description="Rendering Program") + parser.add_argument('--algorithm', type=str, help='Algorithm to use', default=config.get('DEFAULT', 'algorithm')) + parser.add_argument('--scene', type=str, help='Name of the scene to render (without .py).', default=config.get('DEFAULT', 'scene')) + parser.add_argument('--environment', type=str, help='Environment file', default=config.get('DEFAULT', 'environment')) + parser.add_argument('--env_blur', type=str, help='Environment blur', default=config.get('DEFAULT', 'env_blur')) + parser.add_argument('--resolution', type=str, help='Image resolution (WIDTHxHEIGHT)', + default=config.get('DEFAULT', 'resolution')) + parser.add_argument("--samples_per_pixel", type=int, default=config.get('ray_tracing', 'samples_per_pixel'), help="Samples per pixel for rendering.") + parser.add_argument("--output", type=str, default=config.get('DEFAULT', 'output'), help="Output file name.") + + parser.add_argument('--num_spheres', type=int, default=3, help='Number of spheres in the scene for Ray Tracing 0') + parser.add_argument('--num_photons', type=int, default=config.getint('photon_mapping', 'num_photons'), help='Number of photons for photon mapping') + parser.add_argument('--max_depth', type=int, default=config.getint('photon_mapping', 'max_depth'), + help='Maximum depth for photon tracing') + parser.add_argument('--gather_radius', type=float, default=config.getfloat('photon_mapping', 'gather_radius'), + help='Radius for radiance estimation in photon mapping') + + + args = parser.parse_args() + + width, height = parse_resolution(args.resolution) + + # Run the selected algorithm + if args.algorithm == "ray_tracing0": + print("Starting ray tracing zero...") + # ray_trace(args.scene, args.environment_map, image_width=width, image_height=height, output_file="output_ray_traced.png") + ray_trace(args.num_spheres, args.environment_map, image_width=width, image_height=height, # na razie generujemy w kodzie, ale potem trzeba będzie obj wczytywać + output_file="output_ray_traced.png") + elif args.algorithm == "ray_tracing": + print("Starting ray tracing...") + try: + print(args.scene) + scene_module = importlib.import_module(f"scenes.{args.scene}") + except ModuleNotFoundError: + print(f"Error: Scene '{args.scene}' not found in the 'scenes' directory.") + return + try: + scene = scene_module.setup_scene(width=width, height=height, environment=f"{args.environment}") + except AttributeError: + print(f"Error: Scene '{args.scene}' does not define a `setup_scene` function.") + return + # Renderowanie + print(f"Rendering scene '{args.scene}' with {args.samples_per_pixel} samples per pixel...") + img = scene.render(samples_per_pixel=args.samples_per_pixel) + output_path = os.path.join("outputs", args.output) + img.save(output_path) + print(f"Image saved to {output_path}") + img.show() + elif args.algorithm == "photon_mapping": + print("Starting photon mapping...") + image = render_photon_mapping(width, height, args.num_photons, args.max_depth, args.gather_radius) + plt.imshow(image) + plt.axis('off') + output_path = os.path.join("outputs", args.output) + plt.savefig(output_path) + print(f"Image saved to {output_path}") + plt.show() + else: + print(f"Unknown algorithm: {args.algorithm}") + return + +if __name__ == '__main__': + main() diff --git a/Programming/TRAK/output_ray_traced.png b/Programming/TRAK/output_ray_traced.png new file mode 100644 index 00000000..7da47ec3 Binary files /dev/null and b/Programming/TRAK/output_ray_traced.png differ diff --git a/Programming/TRAK/outputs/output.png b/Programming/TRAK/outputs/output.png new file mode 100644 index 00000000..dddfe4ce Binary files /dev/null and b/Programming/TRAK/outputs/output.png differ diff --git a/Programming/TRAK/outputs/output_photonmapping_cube_ball_wall_200x200.png b/Programming/TRAK/outputs/output_photonmapping_cube_ball_wall_200x200.png new file mode 100644 index 00000000..d7cba27e Binary files /dev/null and b/Programming/TRAK/outputs/output_photonmapping_cube_ball_wall_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_cornell_box.png b/Programming/TRAK/outputs/ray_tracing_cornell_box.png new file mode 100644 index 00000000..abb88b64 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_cornell_box.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_cornell_box_200_200x200.png b/Programming/TRAK/outputs/ray_tracing_cornell_box_200_200x200.png new file mode 100644 index 00000000..8156f967 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_cornell_box_200_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_cornell_box_200x200.png b/Programming/TRAK/outputs/ray_tracing_cornell_box_200x200.png new file mode 100644 index 00000000..e8466126 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_cornell_box_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_transparent_cuboid.png b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid.png new file mode 100644 index 00000000..61c53872 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200_200x200.png b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200_200x200.png new file mode 100644 index 00000000..6fe11088 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200x200.png b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200x200.png new file mode 100644 index 00000000..6ec58ab9 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_300x300.png b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_300x300.png new file mode 100644 index 00000000..c96ba70c Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_transparent_cuboid_300x300.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_two_spheres.png b/Programming/TRAK/outputs/ray_tracing_two_spheres.png new file mode 100644 index 00000000..9ea72e38 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_two_spheres.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_two_spheres_200_200x200.png b/Programming/TRAK/outputs/ray_tracing_two_spheres_200_200x200.png new file mode 100644 index 00000000..115c9a21 Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_two_spheres_200_200x200.png differ diff --git a/Programming/TRAK/outputs/ray_tracing_two_spheres_200x200.png b/Programming/TRAK/outputs/ray_tracing_two_spheres_200x200.png new file mode 100644 index 00000000..1d1e3c3b Binary files /dev/null and b/Programming/TRAK/outputs/ray_tracing_two_spheres_200x200.png differ diff --git a/Programming/TRAK/photon_mapping.py b/Programming/TRAK/photon_mapping.py new file mode 100644 index 00000000..befe273c --- /dev/null +++ b/Programming/TRAK/photon_mapping.py @@ -0,0 +1,191 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Define basic vector operations +class Vector3: + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __add__(self, other): + return Vector3(self.x + other.x, self.y + other.y, self.z + other.z) + + def __sub__(self, other): + return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) + + def __mul__(self, scalar): + return Vector3(self.x * scalar, self.y * scalar, self.z * scalar) + + def dot(self, other): + return self.x * other.x + self.y * other.y + self.z * other.z + + def norm(self): + return np.sqrt(self.dot(self)) + + def normalize(self): + n = self.norm() + return Vector3(self.x / n, self.y / n, self.z / n) + + +class Photon: + def __init__(self, position, direction, power): + self.position = position + self.direction = direction + self.power = power + + +class Sphere: + def __init__(self, center, radius, color): + self.center = center + self.radius = radius + self.color = color + + def intersect(self, ray_origin, ray_direction): + oc = ray_origin - self.center + a = ray_direction.dot(ray_direction) + b = 2.0 * oc.dot(ray_direction) + c = oc.dot(oc) - self.radius * self.radius + discriminant = b * b - 4 * a * c + if discriminant < 0: + return None + else: + t = (-b - np.sqrt(discriminant)) / (2.0 * a) + if t < 0: + t = (-b + np.sqrt(discriminant)) / (2.0 * a) + if t < 0: + return None + hit_point = ray_origin + ray_direction * t + normal = (hit_point - self.center).normalize() + return (t, hit_point, normal) + + +class Plane: + def __init__(self, point, normal, color): + self.point = point + self.normal = normal.normalize() + self.color = color + + def intersect(self, ray_origin, ray_direction): + denom = self.normal.dot(ray_direction) + if abs(denom) > 1e-6: + t = (self.point - ray_origin).dot(self.normal) / denom + if t >= 0: + hit_point = ray_origin + ray_direction * t + return (t, hit_point, self.normal) + return None + + +def render_photon_mapping(width, height, num_photons, max_depth, gather_radius): + # Photon mapping logic + photon_map = [] + sphere = Sphere(Vector3(0, 0, -5), 1.0, np.array([1, 0, 0])) # Red sphere + plane = Plane(Vector3(0, -1, 0), Vector3(0, 1, 0), np.array([0.5, 0.5, 0.5])) # Gray plane + objects = [sphere, plane] + + light_position = Vector3(-5, 5, -5) + light_power = np.array([1, 1, 1]) * 1000 + + def emit_photons(): + for _ in range(num_photons): + direction = random_unit_vector() + power = light_power / num_photons + photon = Photon(light_position, direction, power) + trace_photon(photon, 0) + + def trace_photon(photon, depth): + if depth > max_depth: + return + closest_t = np.inf + hit_object = None + hit_info = None + for obj in objects: + result = obj.intersect(photon.position, photon.direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal) + if hit_object: + hit_point, normal = hit_info + photon_map.append((hit_point, photon.power)) + new_direction = random_hemisphere_direction(normal) + photon.position = hit_point + photon.direction = new_direction + photon.power = photon.power * 0.8 + trace_photon(photon, depth + 1) + + def random_unit_vector(): + theta = np.random.uniform(0, 2 * np.pi) + z = np.random.uniform(-1, 1) + r = np.sqrt(1 - z * z) + return Vector3(r * np.cos(theta), r * np.sin(theta), z) + + def random_hemisphere_direction(normal): + dir = random_unit_vector() + if dir.dot(normal) < 0: + dir = Vector3(-dir.x, -dir.y, -dir.z) + return dir + + def trace_ray(ray_origin, ray_direction): + closest_t = np.inf + hit_object = None + hit_info = None + for obj in objects: + result = obj.intersect(ray_origin, ray_direction) + if result: + t, hit_point, normal = result + if t < closest_t: + closest_t = t + hit_object = obj + hit_info = (hit_point, normal, obj.color) + if hit_object: + hit_point, normal, color = hit_info + direct_light = compute_direct_light(hit_point, normal) + indirect_light = estimate_radiance(hit_point, normal) + return color * (direct_light + indirect_light) + else: + return np.array([0, 0, 0]) + + def compute_direct_light(point, normal): + direction_to_light = (light_position - point).normalize() + shadow_origin = point + normal * 1e-5 + shadow_ray = direction_to_light + in_shadow = False + for obj in objects: + result = obj.intersect(shadow_origin, shadow_ray) + if result: + in_shadow = True + break + if in_shadow: + return np.array([0, 0, 0]) + else: + intensity = max(0, normal.dot(direction_to_light)) + return intensity * light_power / (4 * np.pi * (light_position - point).norm() ** 2) + + def estimate_radiance(point, normal): + accumulated_power = np.array([0.0, 0.0, 0.0]) + for photon_pos, photon_power in photon_map: + distance = (photon_pos - point).norm() + if distance < gather_radius: + weight = max(0, normal.dot((photon_pos - point).normalize())) + accumulated_power += photon_power * weight + area = np.pi * gather_radius ** 2 + return accumulated_power / area + + emit_photons() + + aspect_ratio = width / height + fov = np.pi / 3 + image = np.zeros((height, width, 3)) + for y in range(height): + for x in range(width): + px = (2 * (x + 0.5) / width - 1) * np.tan(fov / 2) * aspect_ratio + py = (1 - 2 * (y + 0.5) / height) * np.tan(fov / 2) + ray_origin = Vector3(0, 0, 0) + ray_direction = Vector3(px, py, -1).normalize() + color = trace_ray(ray_origin, ray_direction) + image[y, x, :] = np.clip(color, 0, 1) + + return image diff --git a/Programming/TRAK/ray tracing testy.pdf b/Programming/TRAK/ray tracing testy.pdf new file mode 100644 index 00000000..afa418d4 Binary files /dev/null and b/Programming/TRAK/ray tracing testy.pdf differ diff --git a/Programming/TRAK/rendering.py b/Programming/TRAK/rendering.py new file mode 100644 index 00000000..b0112517 --- /dev/null +++ b/Programming/TRAK/rendering.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +""" Renders an image using raytracing """ +import numpy as np +import matplotlib.pyplot as plt +import time + +def ray_trace(num_spheres, environment, image_width=400, image_height=300, output_file="fig.png"): + IMAGE_WIDTH = image_width + IMAGE_HEIGHT = image_height + + + def normalize(vector): + """ + Normalize a vector. + + Parameters: + vector (numpy.ndarray): The input vector to be normalized. + + Returns: + numpy.ndarray: The normalized vector. + """ + vector /= np.linalg.norm(vector) + return vector + + + def intersect_plane(ray_origin, ray_direction, plane_point, plane_normal): + """ + Calculate the intersection of a ray with a plane. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): A normalized 3D vector representing the + direction of the ray. + plane_point (numpy.ndarray): A 3D point representing a point on the plane. + plane_normal (numpy.ndarray): A normalized 3D vector representing + the normal of the plane. + + Returns: + float: The distance from the origin ray_origin to the intersection + point with the plane. + Returns +inf if there is no intersection or if the intersection is + behind the origin. + """ + denom = np.dot(ray_direction, plane_normal) + if np.abs(denom) < 1e-6: + return np.inf + d = np.dot(plane_point - ray_origin, plane_normal) / denom + if d < 0: + return np.inf + return d + + + def intersect_sphere(ray_origin, ray_direction, sphere_center, sphere_radius): + """ + Calculate the intersection of a ray with a sphere. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): A normalized 3D vector representing the + direction of the ray. + sphere_center (numpy.ndarray): A 3D point representing + the center of the sphere. + sphere_radius (float): The radius of the sphere. + + Returns: + float: The distance from the origin ray_origin to the intersection + point with the sphere. + Returns +inf if there is no intersection or if the intersection is + behind the origin. + """ + a = np.dot(ray_direction, ray_direction) + origin_to_center = ray_origin - sphere_center + b = 2 * np.dot(ray_direction, origin_to_center) + radius_squared = sphere_radius * sphere_radius + c = np.dot(origin_to_center, origin_to_center) - radius_squared + disc = b * b - 4 * a * c + return calculate_sphere_intersection(a, b, c, disc) + + + def calculate_sphere_intersection(a, b, c, disc): + """ + Calculate the + intersection distance of a ray with a sphere using the quadratic formula. + + Parameters: + a (float): Coefficient of t^2 in the quadratic equation. + b (float): Coefficient of t in the quadratic equation. + c (float): Constant term in the quadratic equation. + disc (float): Discriminant of the quadratic equation. + + Returns: + float: + The distance from the origin to the intersection point with the sphere. + Returns +inf if there is no intersection + or if the intersection is behind the origin. + """ + if disc > 0: + distance_squared = np.sqrt(disc) + # q is used to find the roots of the quadratic equation + if b < 0: + q = (-b - distance_squared) / 2.0 + else: + q = (-b + distance_squared) / 2.0 + t0 = q / a + t1 = c / q + t0, t1 = min(t0, t1), max(t0, t1) + if t1 >= 0: + return t1 if t0 < 0 else t0 + return np.inf + + + def intersect(ray_origin, ray_direction, object_): + """ + Calculate the intersection of a ray with an object. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): A normalized 3D vector representing the + direction of the ray. + obj (dict): A dictionary representing the object with keys + 'type', 'position', 'normal' (for planes), and 'radius' (for spheres). + + Returns: + float: The distance from the origin ray_origin to the intersection + point with the object. + Returns +inf if there is no intersection or if the intersection is + behind the origin. + """ + if object_['type'] == 'plane': + return intersect_plane(ray_origin, ray_direction, + object_['position'], object_['normal']) + # object_['type'] == 'sphere': + return intersect_sphere(ray_origin, ray_direction, + object_['position'], object_['radius']) + + + def get_normal(object_, intersection_point): + """ + Calculate the normal at the intersection point on the object. + + Parameters: + obj (dict): A dictionary representing the object with keys + 'type' and 'position'. + intersection_point (numpy.ndarray): A 3D point representing the + intersection point on the object. + + Returns: + numpy.ndarray: The normal vector at the intersection point. + """ + if object_['type'] == 'sphere': + normal = normalize(intersection_point - object_['position']) + elif object_['type'] == 'plane': + normal = object_['normal'] + else: + raise ValueError(f"Unknown object type: {object_['type']}") + return normal + + + def get_color(object_, intersection_point): + """ + Get the color of the object at the intersection point. + + Parameters: + object_ (dict): A dictionary representing the object with a key 'color'. + intersection_point (numpy.ndarray): A 3D point representing the + intersection point on the object. + + Returns: + numpy.ndarray: The color of the object at the intersection point. + """ + color = object_['color'] + if not hasattr(color, '__len__'): + color = color(intersection_point) + return color + + + def trace_ray(ray_origin, ray_direction): + """ + Trace a ray and find the color at the intersection point. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): + A normalized 3D vector representing the direction of the ray. + + Returns: + tuple: A tuple containing the object, + intersection point, normal at the intersection, + and the color at the intersection point. + Returns None if there is no intersection. + """ + t, obj_idx = find_intersection(ray_origin, ray_direction) + if t == np.inf: + return None + object_, intersection_point = get_intersection_details( + ray_origin, ray_direction, t, obj_idx) + normal, color = get_normal(object_, intersection_point), get_color( + object_, intersection_point) + if is_shadowed(intersection_point, normal, obj_idx): + return None + return compute_color( + object_, intersection_point, normal, color, ray_origin) + + + def find_intersection(ray_origin, ray_direction): + """ + Find the intersection of a ray with the objects in the scene. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): + A normalized 3D vector representing the direction of the ray. + + Returns: + tuple: A tuple containing the distance to the intersection point + and the index of the intersected object. + """ + t = np.inf + obj_idx = -1 + for index, object_ in enumerate(scene): + t_obj = intersect(ray_origin, ray_direction, object_) + if t_obj < t: + t, obj_idx = t_obj, index + return t, obj_idx + + + def get_intersection_details(ray_origin, ray_direction, t, obj_idx): + """ + Get the details of the intersection point on the object. + + Parameters: + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + ray_direction (numpy.ndarray): + A normalized 3D vector representing the direction of the ray. + t (float): The distance to the intersection point. + obj_idx (int): The index of the intersected object in the scene. + + Returns: + tuple: A tuple containing the intersected object + and the intersection point. + """ + object_ = scene[obj_idx] + intersection_point = ray_origin + ray_direction * t + return object_, intersection_point + + + def is_shadowed(intersection_point, normal, obj_idx): + """ + Determine if the intersection point is in shadow. + + Parameters: + intersection_point (numpy.ndarray): + A 3D point representing the intersection point on the object. + normal (numpy.ndarray): The normal vector at the intersection point. + obj_idx (int): The index of the intersected object in the scene. + + Returns: + bool: True if the intersection point is in shadow, False otherwise. + """ + to_light = normalize(L - intersection_point) + shadow_intersections = [intersect( + intersection_point + normal * .0001, to_light, obj_sh) + for k, obj_sh in enumerate(scene) if k != obj_idx] + return shadow_intersections and min(shadow_intersections) < np.inf + + + def compute_color(object_, intersection_point, normal, color, ray_origin): + """ + Compute the color at the intersection point using shading techniques. + + Parameters: + object_ (dict): A dictionary representing the intersected object. + intersection_point (numpy.ndarray): + A 3D point representing the intersection point on the object. + normal (numpy.ndarray): The normal vector at the intersection point. + color (numpy.ndarray): The base color of the object. + ray_origin (numpy.ndarray): A 3D point representing the origin of the ray. + + Returns: + tuple: + A tuple containing the intersected object, intersection point, normal, + and the computed color. + """ + to_light = normalize(L - intersection_point) + to_origin = normalize(ray_origin - intersection_point) + color_ray = AMBIENT + diffuse_intensity = object_.get('diffuse_c', DIFFUSE_C) * max( + np.dot(normal, to_light), 0) + color_ray += diffuse_intensity * color + half_vector = normalize(to_light + to_origin) + specular_intensity = object_.get('specular_c', SPECULAR_C) * max( + np.dot(normal, half_vector), 0) ** SPECULAR_K + color_ray += specular_intensity * color_light + return object_, intersection_point, normal, color_ray + + + def add_sphere(position, radius, color): + """ + Create a dictionary representing a sphere object. + + Parameters: + position (list or numpy.ndarray): + A 3D point representing the position of the sphere. + radius (float): The radius of the sphere. + color (list or numpy.ndarray): The color of the sphere. + + Returns: + dict: A dictionary representing the sphere object. + """ + return { + 'type': 'sphere', + 'position': np.array(position), + 'radius': np.array(radius), + 'color': np.array(color), + 'reflection': .5 + } + + + def add_plane(position, normal): + """ + Create a dictionary representing a plane object. + + Parameters: + position (list or numpy.ndarray): + A 3D point representing a point on the plane. + normal (list or numpy.ndarray): + A normalized 3D vector representing the normal of the plane. + + Returns: + dict: A dictionary representing the plane object. + """ + return { + 'type': 'plane', + 'position': np.array(position), + 'normal': np.array(normal), + 'color': lambda M: (color_plane0 + if (int(M[0] * 2) % 2) == (int(M[2] * 2) % 2) + else color_plane1), + 'diffuse_c': .75, + 'specular_c': .5, + 'reflection': .25 + } + + scene = [] + + # List of objects. + color_plane0 = 1. * np.ones(3) + color_plane1 = 0. * np.ones(3) + scene.append(add_plane([0., -0.5, 0.], [0., 1., 0.])) + base_radius = 1 / np.sqrt(num_spheres) # Im więcej kul, tym mniejsze + base_distance = 4.5 / num_spheres + + for i in range(num_spheres): + # Wyliczanie pozycji każdej kuli + x = (i - num_spheres // 2) * base_distance + y = 0.1 + z = 1. + i * 0.5 + + # Dynamiczny kolor (gradient na podstawie indeksu) + color = np.array([i / num_spheres, (num_spheres - i) / num_spheres, 0.5]) + + # Dodanie kuli do sceny + scene.append(add_sphere([x, y, z], base_radius, color)) + + # Light position and color. + L = np.array([5., 5., -10.]) + color_light = np.ones(3) + + # Default light and material parameters. + AMBIENT = .05 + DIFFUSE_C = 1. + SPECULAR_C = 1. + SPECULAR_K = 50 + + DEPTH_MAX = 5 # Maximum number of light reflections. + col = np.zeros(3) # Current color. + camera_origin = np.array([0., 0.35, -1.]) # Camera. + Q = np.array([0., 0., 0.]) # Camera pointing to. + img = np.zeros((IMAGE_HEIGHT, IMAGE_WIDTH, 3)) + + r = float(IMAGE_WIDTH) / IMAGE_HEIGHT + # Screen coordinates: x0, y0, x1, y1. + S = (-1., -1. / r + .25, 1., 1. / r + .25) + + renderTime = time.time() + reflections = 0 + rays = 0 + initialRays = 0 + # Loop through all pixels. + for i, x in enumerate(np.linspace(S[0], S[2], IMAGE_WIDTH)): + if i % 10 == 0: + print(round(i / float(IMAGE_WIDTH) * 100, 2), "%") + for j, y in enumerate(np.linspace(S[1], S[3], IMAGE_HEIGHT)): + col[:] = 0 + Q[:2] = (x, y) + D = normalize(Q - camera_origin) + DEPTH = 0 + rayO, rayD = camera_origin, D + REFLECTION = 1. + initialRays += 1 + # Loop through initial and secondary rays. + while DEPTH < DEPTH_MAX: + traced = trace_ray(rayO, rayD) + rays += 1 + if not traced: + break + reflections += 1 + obj, M, N, col_ray = traced + # Reflection: create a new ray. + rayO, rayD = M + \ + N * .0001, normalize(rayD - 2 * np.dot(rayD, N) * N) + DEPTH += 1 + col += REFLECTION * col_ray + REFLECTION *= obj.get('reflection', 1.) + img[IMAGE_HEIGHT - j - 1, i, :] = np.clip(col, 0, 1) + renderTime = time.time() - renderTime + + plt.imsave(output_file, img) + print(f"Image saved as {output_file}\n" + f"resolution: {IMAGE_WIDTH}x{IMAGE_HEIGHT}\n" + f"render time: {round(renderTime, 2)} s\n" + f"reflections: {reflections}\n" + f"rays (initial): {initialRays}\n" + f"rays (secondary): {rays - initialRays}\n" + f"rays (total): {rays}") diff --git a/Programming/TRAK/requirements.txt b/Programming/TRAK/requirements.txt new file mode 100644 index 00000000..3085b712 --- /dev/null +++ b/Programming/TRAK/requirements.txt @@ -0,0 +1,11 @@ +glfw +numpy +pyrr +PyOpenGL +matplotlib +flake8 +black +autopep8 +flake8-max-function-length +pillow +progressbar \ No newline at end of file diff --git a/Programming/TRAK/scenes/README.md b/Programming/TRAK/scenes/README.md new file mode 100644 index 00000000..96272e79 --- /dev/null +++ b/Programming/TRAK/scenes/README.md @@ -0,0 +1,3 @@ +# Scene directory + +This is scene directory. Put scene files here. \ No newline at end of file diff --git a/Programming/TRAK/scenes/__init__.py b/Programming/TRAK/scenes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Programming/TRAK/scenes/cornell_box.py b/Programming/TRAK/scenes/cornell_box.py new file mode 100644 index 00000000..404ac37e --- /dev/null +++ b/Programming/TRAK/scenes/cornell_box.py @@ -0,0 +1,118 @@ +from sightpy import * + +# define materials to use +def setup_scene(width=400, height=300, environment=None): + + Sc = Scene(ambient_color=rgb(0.00, 0.00, 0.00)) + + angle = -0 + + Sc.add_Camera( + screen_width=width, + screen_height=height, + look_from=vec3(278, 278, 800), + look_at=vec3(278, 278, 0), + focal_distance=1.0, + field_of_view=40, + ) + + # define materials to use + + green_diffuse = Diffuse(diff_color=rgb(0.12, 0.45, 0.15)) + red_diffuse = Diffuse(diff_color=rgb(0.65, 0.05, 0.05)) + white_diffuse = Diffuse(diff_color=rgb(0.73, 0.73, 0.73)) + emissive_white = Emissive(color=rgb(15.0, 15.0, 15.0)) + emissive_blue = Emissive(color=rgb(2.0, 2.0, 3.5)) + blue_glass = Refractive(n=vec3(1.5 + 0.05e-8j, 1.5 + 0.02e-8j, 1.5 + 0.0j)) + + # this is the light + Sc.add( + Plane( + material=emissive_white, + center=vec3(213 + 130 / 2, 554, -227.0 - 105 / 2), + width=130.0, + height=105.0, + u_axis=vec3(1.0, 0.0, 0), + v_axis=vec3(0.0, 0, 1.0), + ), + importance_sampled=True, + ) + + Sc.add( + Plane( + material=white_diffuse, + center=vec3(555 / 2, 555 / 2, -555.0), + width=555.0, + height=555.0, + u_axis=vec3(0.0, 1.0, 0), + v_axis=vec3(1.0, 0, 0.0), + ) + ) + + Sc.add( + Plane( + material=green_diffuse, + center=vec3(-0.0, 555 / 2, -555 / 2), + width=555.0, + height=555.0, + u_axis=vec3(0.0, 1.0, 0), + v_axis=vec3(0.0, 0, -1.0), + ) + ) + + Sc.add( + Plane( + material=red_diffuse, + center=vec3(555.0, 555 / 2, -555 / 2), + width=555.0, + height=555.0, + u_axis=vec3(0.0, 1.0, 0), + v_axis=vec3(0.0, 0, -1.0), + ) + ) + + Sc.add( + Plane( + material=white_diffuse, + center=vec3(555 / 2, 555, -555 / 2), + width=555.0, + height=555.0, + u_axis=vec3(1.0, 0.0, 0), + v_axis=vec3(0.0, 0, -1.0), + ) + ) + + Sc.add( + Plane( + material=white_diffuse, + center=vec3(555 / 2, 0.0, -555 / 2), + width=555.0, + height=555.0, + u_axis=vec3(1.0, 0.0, 0), + v_axis=vec3(0.0, 0, -1.0), + ) + ) + + cb = Cuboid( + material=white_diffuse, + center=vec3(182.5, 165, -285 - 160 / 2), + width=165, + height=165 * 2, + length=165, + shadow=False, + ) + cb.rotate(θ=15, u=vec3(0, 1, 0)) + Sc.add(cb) + + Sc.add( + Sphere( + material=blue_glass, + center=vec3(370.5, 165 / 2, -65 - 185 / 2), + radius=165 / 2, + shadow=False, + max_ray_depth=3, + ), + importance_sampled=True, + ) + + return Sc diff --git a/Programming/TRAK/scenes/soap_bubble.py b/Programming/TRAK/scenes/soap_bubble.py new file mode 100644 index 00000000..7f022618 --- /dev/null +++ b/Programming/TRAK/scenes/soap_bubble.py @@ -0,0 +1,19 @@ +from sightpy import * + +# define materials to use +def setup_scene(width=400, height=300, environment="lake.png"): + # Set Scene + + Sc = Scene(ambient_color=rgb(0.01, 0.01, 0.01)) + + angle = -np.pi * 0.5 + Sc.add_Camera(screen_height=height, screen_width=width, + look_from=vec3(4.0 * np.sin(angle), 0.00, 4.0 * np.cos(angle)), + look_at=vec3(0., 0.05, 0.0)) + + soap_bubble = ThinFilmInterference(thickness=330, noise=60.) + Sc.add(Sphere(material=soap_bubble, center=vec3(1., 0.0, 1.5), radius=1.7, shadow=False, max_ray_depth=5)) + + Sc.add_Background(environment, blur=10.) + + return Sc diff --git a/Programming/TRAK/scenes/three_spheres.py b/Programming/TRAK/scenes/three_spheres.py new file mode 100644 index 00000000..830113a7 --- /dev/null +++ b/Programming/TRAK/scenes/three_spheres.py @@ -0,0 +1,35 @@ +from sightpy import * + +# define materials to use +def setup_scene(width=400, height=300, environment="miramar.jpeg"): + + blue_glass = Refractive(n=vec3(1.5 + 4e-8j, 1.5 + 4e-8j, 1.5 + 0.j)) # n = index of refraction + green_glass = Refractive(n=vec3(1.5 + 4e-8j, 1.5 + 0.j, 1.5 + 4e-8j)) + red_glass = Refractive(n=vec3(1.5 + 0.j, 1.5 + 5e-8j, 1.5 + 5e-8j)) + + floor = Glossy(diff_color=image("checkered_floor.png", repeat=80.), n=vec3(1.2 + 0.3j, 1.2 + 0.3j, 1.1 + 0.3j), + roughness=0.2, spec_coeff=0.3, diff_coeff=0.9) + + # Set Scene + + Sc = Scene(ambient_color=rgb(0.05, 0.05, 0.05)) + + angle = np.pi / 2 * 0.3 + Sc.add_Camera(look_from=vec3(2.5 * np.sin(angle), 0.25, 2.5 * np.cos(angle) - 1.5), + look_at=vec3(0., 0.25, -1.5), + screen_width=width, + screen_height=height) + + Sc.add_DirectionalLight(Ldir=vec3(0.52, 0.45, -0.5), color=rgb(0.15, 0.15, 0.15)) + + Sc.add(Sphere(material=blue_glass, center=vec3(-1.2, 0.0, -1.5), radius=.5, shadow=False, max_ray_depth=3)) + Sc.add(Sphere(material=green_glass, center=vec3(0., 0.0, -1.5), radius=.5, shadow=False, max_ray_depth=3)) + Sc.add(Sphere(material=red_glass, center=vec3(1.2, 0.0, -1.5), radius=.5, shadow=False, max_ray_depth=3)) + + Sc.add(Plane(material=floor, center=vec3(0, -0.5, -3.0), width=120.0, height=120.0, u_axis=vec3(1.0, 0, 0), + v_axis=vec3(0, 0, -1.0), max_ray_depth=3)) + + # see sightpy/backgrounds + Sc.add_Background(environment) + + return Sc diff --git a/Programming/TRAK/scenes/transparent_cuboid.py b/Programming/TRAK/scenes/transparent_cuboid.py new file mode 100644 index 00000000..475cb523 --- /dev/null +++ b/Programming/TRAK/scenes/transparent_cuboid.py @@ -0,0 +1,27 @@ +from sightpy import * + +# define materials to use +def setup_scene(width=400, height=300, environment="stormydays.png"): + floor = Glossy(diff_color=image("checkered_floor.png", repeat=2.), roughness=0.2, spec_coeff=0.3, diff_coeff=0.7, + n=vec3(2.2, 2.2, 2.2)) # n = index of refraction + green_glass = Refractive(n=vec3(1.5 + 4e-8j, 1.5 + 0.j, 1.5 + 4e-8j)) + + Sc = Scene() + Sc.add_Camera(look_from=vec3(0., 0.25, 1.), look_at=vec3(0., 0.25, -3.), + screen_width=width, + screen_height=height) + + Sc.add_DirectionalLight(Ldir=vec3(0.0, 0.5, 0.5), color=rgb(0.5, 0.5, 0.5)) + + Sc.add(Plane(material=floor, center=vec3(0, -0.5, -3.0), width=6.0, height=6.0, u_axis=vec3(1.0, 0, 0), + v_axis=vec3(0, 0, -1.0), max_ray_depth=5)) + + cb = Cuboid(material=green_glass, center=vec3(0.00, 0.0001, -0.8), width=0.9, height=1.0, length=0.4, shadow=False, + max_ray_depth=5) + cb.rotate(θ=30, u=vec3(0, 1, 0)) + Sc.add(cb) + + # see sightpy/backgrounds + Sc.add_Background(environment) + + return Sc \ No newline at end of file diff --git a/Programming/TRAK/scenes/two_spheres.py b/Programming/TRAK/scenes/two_spheres.py new file mode 100644 index 00000000..846bea3c --- /dev/null +++ b/Programming/TRAK/scenes/two_spheres.py @@ -0,0 +1,38 @@ +from sightpy import * + +# define materials to use +def setup_scene(width=400, height=300, environment="stormydays.png"): + gold_metal = Glossy(diff_color = rgb(1., .572, .184), n = vec3(0.15+3.58j, 0.4+2.37j, 1.54+1.91j), roughness = 0.0, spec_coeff = 0.2, diff_coeff= 0.8) # n = index of refraction + bluish_metal = Glossy(diff_color = rgb(0.0, 0, 0.1), n = vec3(1.3+1.91j, 1.3+1.91j, 1.4+2.91j), roughness = 0.2,spec_coeff = 0.5, diff_coeff= 0.3) + + floor = Glossy(diff_color = image("checkered_floor.png", repeat = 80.), + n = vec3(1.2+ 0.3j, 1.2+ 0.3j, 1.1+ 0.3j), roughness = 0.2, spec_coeff = 0.3, diff_coeff= 0.9 ) + + + + + + # Set Scene + Sc = Scene(ambient_color = rgb(0.05, 0.05, 0.05)) + + + angle = -np.pi/2 * 0.3 + Sc.add_Camera(look_from = vec3(2.5*np.sin(angle), 0.25, 2.5*np.cos(angle) -1.5 ), + look_at = vec3(0., 0.25, -3.), + screen_width = width , + screen_height = height) + + + + Sc.add_DirectionalLight(Ldir = vec3(0.52,0.45, -0.5), color = rgb(0.15, 0.15, 0.15)) + + + Sc.add(Sphere(material = gold_metal, center = vec3(-.75, .1, -3.),radius = .6, max_ray_depth = 3)) + Sc.add(Sphere(material = bluish_metal, center = vec3(1.25, .1, -3.), radius = .6, max_ray_depth = 3)) + + Sc.add(Plane(material = floor, center = vec3(0, -0.5, -3.0), width = 120.0,height = 120.0, u_axis = vec3(1.0, 0, 0), v_axis = vec3(0, 0, -1.0), max_ray_depth = 3)) + + #see sightpy/backgrounds + Sc.add_Background(environment) + + return Sc \ No newline at end of file diff --git a/Programming/TRAK/sightpy/__init__.py b/Programming/TRAK/sightpy/__init__.py new file mode 100644 index 00000000..19c250df --- /dev/null +++ b/Programming/TRAK/sightpy/__init__.py @@ -0,0 +1,17 @@ +from .utils.constants import * +from .utils.vector3 import * +from .utils.colour_functions import * +from .utils.image_functions import * + +from .ray import * +from .scene import * +from .geometry import * +from .lights import * +from .materials import * +from .textures.texture import * +from .animation import * + + + + + diff --git a/Programming/TRAK/sightpy/animation.py b/Programming/TRAK/sightpy/animation.py new file mode 100644 index 00000000..30411a9b --- /dev/null +++ b/Programming/TRAK/sightpy/animation.py @@ -0,0 +1,54 @@ +from .scene import Scene +import numpy as np +from pathlib import Path + + +def create_animation(scene,samples_per_pixel, fps, start_time, final_time, update_scene, name): + + """ + this function render a list of frames and saves them in ./frames folder. You can make an animation the using ffmpeg running + from the command prompt: + """ + #ffmpeg -r 60 -f image2 -s 854x480 -i your_image_%d.png -vcodec libx264 -crf 1 -pix_fmt yuv420p your_video.mp4 + #fps #resoluion #crf = quality (less is better) + + + number_of_frames = int(fps*(final_time - start_time)) + dt = (final_time - start_time)/number_of_frames + t = start_time + + try: + Path("./frames").mkdir() + + except FileExistsError: + pass + + + for i in range(0,number_of_frames): + update_scene(scene, t) + img = scene.render(samples_per_pixel) + t += dt + img.save("frames/" + name + "_" + str(i) + ".png") + + + + +def create_animation_using_opencv(scene, samples_per_pixel , fps, start_time, final_time, update_scene, name): + + import cv2 + number_of_frames = int(fps*(final_time - start_time)) + dt = (final_time - start_time)/number_of_frames + t = start_time + + + videodims = (scene.camera.screen_width, scene.camera.screen_height) + fourcc = cv2.VideoWriter_fourcc('M', 'J', 'P', 'G') + video = cv2.VideoWriter(name,fourcc, fps,videodims) + + for i in range(0,number_of_frames): + update_scene(scene, t) + frame = scene.render(samples_per_pixel) + video.write(cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)) + t += dt + + video.release() diff --git a/Programming/TRAK/sightpy/backgrounds/__init__.py b/Programming/TRAK/sightpy/backgrounds/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Programming/TRAK/sightpy/backgrounds/lake.png b/Programming/TRAK/sightpy/backgrounds/lake.png new file mode 100644 index 00000000..f7541a2c Binary files /dev/null and b/Programming/TRAK/sightpy/backgrounds/lake.png differ diff --git a/Programming/TRAK/sightpy/backgrounds/lightmaps/lake.png b/Programming/TRAK/sightpy/backgrounds/lightmaps/lake.png new file mode 100644 index 00000000..349a96a6 Binary files /dev/null and b/Programming/TRAK/sightpy/backgrounds/lightmaps/lake.png differ diff --git a/Programming/TRAK/sightpy/backgrounds/miramar.jpeg b/Programming/TRAK/sightpy/backgrounds/miramar.jpeg new file mode 100644 index 00000000..1a720c20 Binary files /dev/null and b/Programming/TRAK/sightpy/backgrounds/miramar.jpeg differ diff --git a/Programming/TRAK/sightpy/backgrounds/panorama.py b/Programming/TRAK/sightpy/backgrounds/panorama.py new file mode 100644 index 00000000..3eb9867f --- /dev/null +++ b/Programming/TRAK/sightpy/backgrounds/panorama.py @@ -0,0 +1,18 @@ +from ..geometry import Sphere_Collider, Primitive +from ..materials import Material +from ..utils.vector3 import vec3 +from ..utils.constants import SKYBOX_DISTANCE +from ..utils.image_functions import load_image, load_image_as_linear_sRGB +from .util.blur_background import blur_skybox +from .skybox import SkyBox_Material + +class Panorama(Primitive): + def __init__(self, panorama, center = vec3(0.,0.,0.), light_intensity = 0.0, blur = 0.0): + super().__init__(center, SkyBox_Material(panorama, light_intensity, blur), shadow = False) + l = SKYBOX_DISTANCE + self.light_intensity = light_intensity + self.collider_list += [Sphere_Collider(assigned_primitive = self, center = center , radius = SKYBOX_DISTANCE)] + + + def get_uv(self, hit): + return hit.collider.get_uv(hit) \ No newline at end of file diff --git a/Programming/TRAK/sightpy/backgrounds/skybox.py b/Programming/TRAK/sightpy/backgrounds/skybox.py new file mode 100644 index 00000000..93802d37 --- /dev/null +++ b/Programming/TRAK/sightpy/backgrounds/skybox.py @@ -0,0 +1,57 @@ +from ..geometry import Cuboid_Collider, Primitive +from ..materials import Material +from ..utils.vector3 import vec3 +from ..utils.constants import SKYBOX_DISTANCE +from ..utils.image_functions import load_image, load_image_as_linear_sRGB +from .util.blur_background import blur_skybox + +class SkyBox(Primitive): + def __init__(self, cubemap, center = vec3(0.,0.,0.), light_intensity = 0.0, blur = 0.0): + super().__init__(center, SkyBox_Material(cubemap, light_intensity, blur), shadow = False) + l = SKYBOX_DISTANCE + self.light_intensity = light_intensity + #BOTTOM + self.collider_list += [Cuboid_Collider(assigned_primitive = self, center = center, width = 2*l, height =2*l ,length =2*l )] + + + def get_uv(self, hit): + u,v = hit.collider.get_uv(hit) + u,v = u/4,v/3 + return u,v + + +class SkyBox_Material(Material): + def __init__(self, cubemap, light_intensity, blur): + self.texture = load_image_as_linear_sRGB("sightpy/backgrounds/" + cubemap) + + if light_intensity != 0.0: + self.lightmap = load_image("sightpy/backgrounds/lightmaps/" + cubemap) + + if blur != 0.0: + self.blur_image = blur_skybox(load_image("sightpy/backgrounds/" + cubemap), blur, cubemap) + + self.blur = blur + self.light_intensity = light_intensity + self.repeat = 1.0 + + def get_texture_color(self, hit, ray): + u,v = hit.get_uv() + + if (self.blur != 0.0) : + im = self.blur_image[-((v * self.blur_image.shape[0]*self.repeat ).astype(int)% self.blur_image.shape[0]) , (u * self.blur_image.shape[1]*self.repeat).astype(int) % self.blur_image.shape[1] ].T + else: + im = self.texture[-((v * self.texture.shape[0]*self.repeat ).astype(int)% self.texture.shape[0]) , (u * self.texture.shape[1]*self.repeat).astype(int) % self.texture.shape[1] ].T + + if (ray.depth != 0) and (self.light_intensity != 0.0): + ls = self.lightmap[-((v * self.texture.shape[0]*self.repeat ).astype(int)% self.texture.shape[0]) , (u * self.texture.shape[1]*self.repeat).astype(int) % self.texture.shape[1] ].T + color = vec3(im[0] + self.light_intensity * ls[0], im[1] + self.light_intensity * ls[1], im[2] + self.light_intensity * ls[2]) + + else: + color = vec3(im[0] , im[1] , im[2] ) + return color + + + def get_color(self, scene, ray, hit): + hit.point = (ray.origin + ray.dir * hit.distance) + return hit.material.get_texture_color(hit,ray) + diff --git a/Programming/TRAK/sightpy/backgrounds/stormydays.png b/Programming/TRAK/sightpy/backgrounds/stormydays.png new file mode 100644 index 00000000..0e76d1a1 Binary files /dev/null and b/Programming/TRAK/sightpy/backgrounds/stormydays.png differ diff --git a/Programming/TRAK/sightpy/backgrounds/util/__init__.py b/Programming/TRAK/sightpy/backgrounds/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Programming/TRAK/sightpy/backgrounds/util/blur_background.py b/Programming/TRAK/sightpy/backgrounds/util/blur_background.py new file mode 100644 index 00000000..02f6ca5d --- /dev/null +++ b/Programming/TRAK/sightpy/backgrounds/util/blur_background.py @@ -0,0 +1,158 @@ +from PIL import Image, ImageFilter +import numpy as np +from ...utils.colour_functions import sRGB_to_sRGB_linear + + + +def to_image(arr): + img = [Image.fromarray((255 * arr[:,:,i]).astype(np.uint8), "L") for i in range(0,3)] + return Image.merge("RGB", img) + +def to_array(img): + return np.asarray(img)/256. + + + + + +def blur_skybox(img_array, blur, cubemap): + + print("blurring " + cubemap) + + N = int(img_array.shape[0]/3) + + left = img_array[ 1*N:2*N, 0*N:1*N] + front = img_array[N:2*N, N:2*N] + right = img_array[ N:2*N, 2*N:3*N] + back = img_array[N:2*N, 3*N:4*N] + top = img_array[0:1*N, 1*N:2*N] + bottom = img_array[2*N:3*N, 1*N:2*N] + + + cubelist = [[None,top,None, None], + [left,front,right, back], + [None,bottom,None, None]] + + + + back_blur = np.zeros((N*3, N*3,3)) + + + back_blur[ 1*N:2*N, 0*N:1*N] = cubelist[1][0-2] #left + back_blur[N:2*N, N:2*N] = cubelist[1][1-2] #front + back_blur[ N:2*N, 2*N:3*N] = cubelist[1][2-2] # right + back_blur[2*N:3*N, 1*N:2*N] = np.rot90(cubelist[2][1] , k=2) #bottom + back_blur[0:1*N, 1*N:2*N] = np.rot90(cubelist[0][1], k=2) #top + + + back_blur = to_image(back_blur) + back_blur = (back_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + back_blur = to_array(back_blur) + + back_blur = back_blur[N:2*N, N:2*N] + + + top_blur = np.zeros((N*3, N*3,3)) + top_blur[ 1*N:2*N, 0*N:1*N] = np.rot90(cubelist[1][0], k=-1) #left + top_blur[N:2*N, N:2*N] = cubelist[1-1][1] #front + top_blur[ N:2*N, 2*N:3*N] = np.rot90(cubelist[1][2], k=1) # right + top_blur[2*N:3*N, 1*N:2*N] = cubelist[1][1] #bottom + top_blur[0:1*N, 1*N:2*N] = np.rot90(cubelist[1][3], k=2) #top + top_blur = to_image(top_blur) + top_blur = (top_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + top_blur = to_array(top_blur) + + top_blur = top_blur[N:2*N, N:2*N] + + + + + bottom_blur = np.zeros((N*3, N*3,3)) + + + bottom_blur[ 1*N:2*N, 0*N:1*N] = np.rot90(cubelist[1][0], k=1) #left + bottom_blur[N:2*N, N:2*N] = cubelist[1+1][1] #front + bottom_blur[ N:2*N, 2*N:3*N] = np.rot90(cubelist[1][2], k=-1) # right + bottom_blur[2*N:3*N, 1*N:2*N] = np.rot90(cubelist[1][3], k=2) #bottom + bottom_blur[0:1*N, 1*N:2*N] = cubelist[1][1] #top + + bottom_blur = to_image(bottom_blur) + bottom_blur = (bottom_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + bottom_blur = to_array(bottom_blur) + + bottom_blur = bottom_blur[N:2*N, N:2*N] + + + + right_blur = np.zeros((N*3, N*3,3)) + + right_blur[ 1*N:2*N, 0*N:1*N] = cubelist[1][0+1] #left + right_blur[N:2*N, N:2*N] = cubelist[1][1+1] #front + right_blur[ N:2*N, 2*N:3*N] = cubelist[1][2+1] # right + right_blur[2*N:3*N, 1*N:2*N] = np.rot90(cubelist[2][1] ) #bottom + right_blur[0:1*N, 1*N:2*N] = np.rot90(cubelist[0][1], k=-1) #top + + right_blur = to_image(right_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + + right_blur = to_array(right_blur) + + right_blur = right_blur[N:2*N, N:2*N] + + + + front_blur = np.zeros((N*3, N*3,3)) + + + front_blur [ 1*N:2*N, 0*N:1*N] = cubelist[1][0] #left + front_blur [N:2*N, N:2*N] = cubelist[1][1] #front + front_blur [ N:2*N, 2*N:3*N] = cubelist[1][2] # right + front_blur [2*N:3*N, 1*N:2*N] = cubelist[2][1] #bottom + front_blur [0:1*N, 1*N:2*N] = cubelist[0][1] #top + + front_blur = to_image(front_blur) + + front_blur = (front_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + front_blur = to_array(front_blur) + + front_blur = front_blur[N:2*N, N:2*N] + + + + + left_blur = np.zeros((N*3, N*3,3)) + + left_blur[ 1*N:2*N, 0*N:1*N] = cubelist[1][0-1] #left + left_blur[N:2*N, N:2*N] = cubelist[1][1-1] #front + left_blur[ N:2*N, 2*N:3*N] = cubelist[1][2-1] + left_blur[2*N:3*N, 1*N:2*N] = np.rot90(cubelist[2][1], k=-1 ) #bottom + left_blur[0:1*N, 1*N:2*N] = np.rot90(cubelist[0][1], k=1) #top + + + left_blur = to_image(left_blur).filter(ImageFilter.GaussianBlur(radius=blur)) + + + left_blur = to_array(left_blur) + + left_blur = left_blur[N:2*N, N:2*N] + + + + + + + skybox_blurred = np.zeros((N*3, N*4,3)) + + skybox_blurred[ 1*N:2*N, 0*N:1*N] = left_blur + skybox_blurred[N:2*N, N:2*N] = front_blur + skybox_blurred[ N:2*N, 2*N:3*N] = right_blur + skybox_blurred[N:2*N, 3*N:4*N] = back_blur + skybox_blurred[0:1*N, 1*N:2*N] = top_blur + skybox_blurred[2*N:3*N, 1*N:2*N] = bottom_blur + + return sRGB_to_sRGB_linear(skybox_blurred) + diff --git a/Programming/TRAK/sightpy/camera.py b/Programming/TRAK/sightpy/camera.py new file mode 100644 index 00000000..0f29133a --- /dev/null +++ b/Programming/TRAK/sightpy/camera.py @@ -0,0 +1,52 @@ +from .utils.vector3 import vec3, rgb +from .utils.random import random_in_unit_disk +import numpy as np +from .ray import Ray + +class Camera(): + def __init__(self, look_from, look_at, screen_width = 400 ,screen_height = 300, field_of_view = 90., aperture = 0., focal_distance = 1.): + self.screen_width = screen_width + self.screen_height = screen_height + self.aspect_ratio = float(screen_width) / screen_height + + self.look_from = look_from + self.look_at = look_at + self.camera_width = np.tan(field_of_view * np.pi/180 /2.)*2. + self.camera_height = self.camera_width/self.aspect_ratio + + #camera reference basis in world coordinates + self.cameraFwd = (look_at - look_from).normalize() + self.cameraRight = (self.cameraFwd.cross(vec3(0.,1.,0.))).normalize() + self.cameraUp = self.cameraRight.cross(self.cameraFwd) + + + + #if you use a lens_radius >= 0.0 make sure that samples_per_pixel is a large number. Otherwise you'll get a lot of noise + self.lens_radius = aperture / 2. + self.focal_distance = focal_distance + + + + # Pixels coordinates in camera basis: + self.x = np.linspace(-self.camera_width/2., self.camera_width/2., self.screen_width) + self.y = np.linspace(self.camera_height/2., -self.camera_height/2., self.screen_height) + + # we are going to cast a total of screen_width * screen_height * samples_per_pixel rays + # xx,yy store the origin of each ray in a 3d array where the first and second dimension are the x,y coordinates of each pixel + # and the third dimension is the sample index of each pixel + xx,yy = np.meshgrid(self.x,self.y) + self.x = xx.flatten() + self.y = yy.flatten() + + def get_ray(self,n): # n = index of refraction of scene main medium (for air n = 1.) + + # in each pixel, take a random position to avoid aliasing. + x = self.x + (np.random.rand(len(self.x )) - 0.5)*self.camera_width /(self.screen_width) + y = self.y + (np.random.rand(len(self.y )) - 0.5)*self.camera_height /(self.screen_height) + + # set ray direction in world space: + rx, ry = random_in_unit_disk(x.shape[0]) + ray_origin = self.look_from + self.cameraRight *rx* self.lens_radius + self.cameraUp *ry* self.lens_radius + ray_dir = (self.look_from + self.cameraUp*y*self.focal_distance + self.cameraRight*x*self.focal_distance + self.cameraFwd*self.focal_distance - ray_origin ).normalize() + return Ray(origin=ray_origin, dir=ray_dir, depth=0, n=n, reflections = 0, transmissions = 0, diffuse_reflections = 0) + diff --git a/Programming/TRAK/sightpy/geometry/__init__.py b/Programming/TRAK/sightpy/geometry/__init__.py new file mode 100644 index 00000000..cdeaa1bd --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/__init__.py @@ -0,0 +1,13 @@ +from .primitive import * +from .collider import * +from .sphere import * +from .plane import * +from .triangle import * +from .triangle_mesh import * +from .cuboid import * + + + + + + diff --git a/Programming/TRAK/sightpy/geometry/collider.py b/Programming/TRAK/sightpy/geometry/collider.py new file mode 100644 index 00000000..97a500fa --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/collider.py @@ -0,0 +1,17 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from abc import abstractmethod + +class Collider: + def __init__(self,assigned_primitive, center): + self.assigned_primitive = assigned_primitive + self.center = center + + @abstractmethod + def intersect(self, O, D): + pass + + @abstractmethod + def get_Normal(self, hit): + pass \ No newline at end of file diff --git a/Programming/TRAK/sightpy/geometry/cuboid.py b/Programming/TRAK/sightpy/geometry/cuboid.py new file mode 100644 index 00000000..086b5231 --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/cuboid.py @@ -0,0 +1,145 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from ..geometry import Primitive, Collider + +class Cuboid(Primitive): + def __init__(self,center, material, width,height, length,max_ray_depth = 5, shadow = True): + super().__init__(center, material, max_ray_depth, shadow = shadow) + self.width = width + self.height = height + self.length = length + self.bounded_sphere_radius = np.sqrt((self.width/2)**2 + (self.height/2)**2 + (self.length/2)**2) + + self.collider_list += [Cuboid_Collider(assigned_primitive = self, center = center, width = width, height =height ,length =length )] + + + def get_uv(self, hit): + u,v = hit.collider.get_uv(hit) + u,v = u/4,v/3 + return u,v + + +""" + This was the old approach, but remplaced by a Box collider that is more efficient + #we model a cuboid as six planes + + + #BOTTOM #BOTTOM + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(0.0,-h, 0.0), u_axis = vec3(1.0, 0.0, 0.0), v_axis = vec3(0.0, 0.0, 1.0), w = w, h = l, uv_shift = (1,0))] + #TOP #TOP + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(0.0,h, 0.0), u_axis = vec3(1.0, 0.0, 0.0), v_axis = vec3(0.0, 0.0, -1.0), w = w, h = l, uv_shift= (1,2))] + #RIGHT #RIGHT + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(w,0.0, 0.0), u_axis = vec3(0.0, 0.0, -1.0), v_axis = vec3(0.0, 1.0, 0.0), w = l, h = h, uv_shift= (2,1))] + #LEFT #LEFT + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(-w,0.0, 0.0), u_axis = vec3(0.0, 0.0, 1.0), v_axis = vec3(0.0, 1.0, 0.0), w = l, h = h, uv_shift= (0,1))] + #FRONT #FRONT + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(0,0, l), u_axis = vec3(1.0, 0.0, 0.0), v_axis = vec3(0.0, 1.0, 0.0), w = w, h = h, uv_shift= (1,1))] + #BACK #BACK + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center + vec3(0,0, -l), u_axis = vec3(-1.0, 0.0, 0.0), v_axis = vec3(0.0, 1.0, 0.0), w = w, h = h, uv_shift= (3,1))] +""" + + +class Cuboid_Collider(Collider): + def __init__(self, width, height,length,**kwargs): + super().__init__(**kwargs) + + self.lb = self.center - vec3(width/2, height/2, length/2) + self.rt = self.center + vec3(width/2, height/2, length/2) + + self.lb_local_basis = self.lb + self.rt_local_basis = self.rt + + self.width = width + self.height = height + self.length = length + + # basis vectors + self.ax_w = vec3(1.,0.,0.) + self.ax_h = vec3(0.,1.,0.) + self.ax_l = vec3(0.,0.,1.) + + self.inverse_basis_matrix = np.array([[self.ax_w.x, self.ax_h.x, self.ax_l.x], + [self.ax_w.y, self.ax_h.y, self.ax_l.y], + [self.ax_w.z, self.ax_h.z, self.ax_l.z]]) + + self.basis_matrix = self.inverse_basis_matrix.T + + + def rotate(self,M, center): + self.ax_w = self.ax_w.matmul(M) + self.ax_h = self.ax_h.matmul(M) + self.ax_l = self.ax_l.matmul(M) + + self.inverse_basis_matrix = np.array([[self.ax_w.x, self.ax_h.x, self.ax_l.x], + [self.ax_w.y, self.ax_h.y, self.ax_l.y], + [self.ax_w.z, self.ax_h.z, self.ax_l.z]]) + + self.basis_matrix = self.inverse_basis_matrix.T + + self.lb = center + (self.lb-center).matmul(M) + self.rt = center + (self.rt-center).matmul(M) + + self.lb_local_basis = self.lb.matmul(self.basis_matrix) + self.rt_local_basis = self.rt.matmul(self.basis_matrix) + + def intersect(self, O, D): + + + O_local_basis = O.matmul(self.basis_matrix) + D_local_basis = D.matmul(self.basis_matrix) + + dirfrac = 1.0 / D_local_basis + + # lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner + t1 = (self.lb_local_basis.x - O_local_basis.x)*dirfrac.x; + t2 = (self.rt_local_basis.x - O_local_basis.x)*dirfrac.x; + t3 = (self.lb_local_basis.y - O_local_basis.y)*dirfrac.y; + t4 = (self.rt_local_basis.y - O_local_basis.y)*dirfrac.y; + t5 = (self.lb_local_basis.z - O_local_basis.z)*dirfrac.z; + t6 = (self.rt_local_basis.z - O_local_basis.z)*dirfrac.z; + + tmin = np.maximum(np.maximum(np.minimum(t1, t2), np.minimum(t3, t4)), np.minimum(t5, t6)) + tmax = np.minimum(np.minimum(np.maximum(t1, t2), np.maximum(t3, t4)), np.maximum(t5, t6)) + + # if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us + # if tmin > tmax, ray doesn't intersect AAB + mask1 = (tmax < 0) | (tmin > tmax) + + # if tmin < 0 then the ray origin is inside of the AABB and tmin is behind the start of the ray so tmax is the first intersection + mask2 = tmin < 0 + return np.select([mask1,mask2,True] , [FARAWAY , [tmax, np.tile(UPDOWN, tmin.shape)] , [tmin, np.tile(UPWARDS, tmin.shape)]]) + + + def get_Normal(self, hit): + + P = (hit.point-self.center).matmul(self.basis_matrix) + absP = vec3(1./self.width, 1./self.height, 1./self.length)*np.abs(P) + Pmax = np.maximum(np.maximum(absP.x, absP.y), absP.z) + P.x = np.where(Pmax == absP.x, np.sign(P.x), 0.) + P.y = np.where(Pmax == absP.y, np.sign(P.y), 0.) + P.z = np.where(Pmax == absP.z, np.sign(P.z), 0.) + + return P.matmul(self.inverse_basis_matrix) + + def get_uv(self, hit): + hit.N = self.get_Normal(hit) + M_C = hit.point - self.center + + BOTTOM = (hit.N == vec3(0.,-1.,0.)) + TOP = (hit.N == vec3(0., 1.,0.)) + RIGHT = (hit.N == vec3(1.,0.,0.)) + LEFT = (hit.N == vec3(-1.,0.,0.) ) + FRONT = (hit.N == vec3(0.,0.,1.)) + BACK = (hit.N == vec3(0.,0.,-1.)) + + #0.985 to avoid corners + u = np.select([BOTTOM , TOP, RIGHT, LEFT , FRONT , BACK], [((self.ax_w.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1), ((self.ax_w.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1), ((self.ax_l.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 2), (((self.ax_l*-1).dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 0), (((self.ax_w*-1).dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 3), (( self.ax_w.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1)]) + v = np.select([BOTTOM , TOP, RIGHT, LEFT , FRONT , BACK], [(((self.ax_l*-1).dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 0), ((self.ax_l.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 2), ((self.ax_h.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1), (((self.ax_h).dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1), (((self.ax_h).dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1), (( self.ax_h.dot(M_C)/self.width*2 *0.985 + 1 ) /2 + 1)]) + return u,v + + + + + + diff --git a/Programming/TRAK/sightpy/geometry/plane.py b/Programming/TRAK/sightpy/geometry/plane.py new file mode 100644 index 00000000..9af46b1c --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/plane.py @@ -0,0 +1,81 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from ..geometry import Primitive, Collider + + +class Plane(Primitive): + def __init__(self,center, material, width,height, u_axis, v_axis, max_ray_depth = 5, shadow = True): + super().__init__(center, material, max_ray_depth,shadow = shadow) + self.collider_list += [Plane_Collider(assigned_primitive = self, center = center, u_axis = u_axis, v_axis = v_axis, w= width/2, h=height/2)] + self.width = width + self.height = height + self.bounded_sphere_radius = np.sqrt((width/2)**2 + (height/2)**2) + + def get_uv(self, hit): + return hit.collider.get_uv(hit) + + +class Plane_Collider(Collider): + def __init__(self, u_axis, v_axis, w, h, uv_shift = (0.,0.),**kwargs): + super().__init__(**kwargs) + self.normal = u_axis.cross(v_axis).normalize() + + + self.w = w + self.h = h + self.u_axis = u_axis + self.v_axis = v_axis + self.uv_shift = uv_shift + self.inverse_basis_matrix = np.array([[self.u_axis.x, self.v_axis.x, self.normal.x], + [self.u_axis.y, self.v_axis.y, self.normal.y], + [self.u_axis.z, self.v_axis.z, self.normal.z]]) + self.basis_matrix = self.inverse_basis_matrix.T + + + + + def intersect(self, O, D): + N = self.normal + + NdotD = N.dot(D) + NdotD = np.where(NdotD == 0., NdotD + 0.0001, NdotD) #avoid zero division + + NdotC_O = N.dot(self.center - O) + d = D * NdotC_O / NdotD + M = O + d # intersection point + dis = d.length() + + M_C = M - self.center + + #plane basis coordinates + u = self.u_axis.dot(M_C) + v = self.v_axis.dot(M_C) + + + hit_inside = (np.abs(u) <= self.w) & (np.abs(v) <= self.h) & (NdotC_O * NdotD > 0) + hit_UPWARDS = (NdotD < 0) + hit_UPDOWN = np.logical_not(hit_UPWARDS) + + + pred1 = hit_inside & hit_UPWARDS + pred2 = hit_inside & hit_UPDOWN + pred3 = True + return np.select([pred1,pred2,pred3] , [[dis, np.tile(UPWARDS, dis.shape) ], [dis,np.tile(UPDOWN, dis.shape)], FARAWAY]) + + def rotate(self,M, center): + self.u_axis = self.u_axis.matmul(M) + self.v_axis = self.v_axis.matmul(M) + self.normal = self.normal.matmul(M) + self.center = center + (self.center-center).matmul(M) + + def get_uv(self, hit): + M_C = hit.point - self.center + u = ((self.u_axis.dot(M_C)/self.w + 1 ) /2 + self.uv_shift[0]) + v = ((self.v_axis.dot(M_C)/self.h + 1 ) /2 + self.uv_shift[1]) + return u,v + + + def get_Normal(self, hit): + return self.normal + diff --git a/Programming/TRAK/sightpy/geometry/primitive.py b/Programming/TRAK/sightpy/geometry/primitive.py new file mode 100644 index 00000000..9895a942 --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/primitive.py @@ -0,0 +1,30 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3 +import numpy as np + + +class Primitive: + def __init__(self, center, material, max_ray_depth = 1, shadow = True): + self.center = center + self.material = material + self.material.assigned_primitive = self + self.shadow = shadow + self.collider_list = [] + self.max_ray_depth = max_ray_depth + + def rotate(self, θ, u): + + u = u.normalize() + θ = θ/180 *np.pi + cosθ = np.cos(θ) + sinθ = np.sqrt(1-cosθ**2) * np.sign(θ) + + #rotation matrix along u axis + M = np.array([ + [cosθ + u.x*u.x * (1-cosθ), u.x*u.y*(1-cosθ) - u.z*sinθ, u.x*u.z*(1-cosθ) +u.y*sinθ], + [u.y*u.x*(1-cosθ) + u.z*sinθ, cosθ + u.y**2 * (1-cosθ), u.y*u.z*(1-cosθ) -u.x*sinθ], + [u.z*u.x*(1-cosθ) -u.y*sinθ, u.z*u.y*(1-cosθ) + u.x*sinθ, cosθ + u.z*u.z*(1-cosθ)] + ]) + for c in self.collider_list: + c.rotate(M, self.center) + diff --git a/Programming/TRAK/sightpy/geometry/sphere.py b/Programming/TRAK/sightpy/geometry/sphere.py new file mode 100644 index 00000000..1d748a02 --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/sphere.py @@ -0,0 +1,52 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from ..geometry import Primitive, Collider + +class Sphere(Primitive): + def __init__(self,center, material, radius, max_ray_depth = 5, shadow = True): + super().__init__(center, material, max_ray_depth, shadow = shadow) + self.collider_list += [Sphere_Collider(assigned_primitive = self, center = center, radius = radius)] + self.bounded_sphere_radius = radius + + def get_uv(self, hit): + return hit.collider.get_uv(hit) + + +class Sphere_Collider(Collider): + def __init__(self, radius, **kwargs): + super().__init__(**kwargs) + self.radius = radius + + + def intersect(self, O, D): + + b = 2 * D.dot(O - self.center) + c = self.center.square_length() + O.square_length() - 2 * self.center.dot(O) - (self.radius * self.radius) + disc = (b ** 2) - (4 * c) + sq = np.sqrt(np.maximum(0, disc)) + h0 = (-b - sq) / 2 + h1 = (-b + sq) / 2 + h = np.where((h0 > 0) & (h0 < h1), h0, h1) + pred = (disc > 0) & (h > 0) + M = (O + D * h) + NdotD = ((M - self.center) * (1. / self.radius) ).dot(D) + + pred1 = (disc > 0) & (h > 0) & (NdotD > 0) + pred2 = (disc > 0) & (h > 0) & (NdotD < 0) + pred3 = True + + #return an array with hit distance and the hit orientation + return np.select([pred1,pred2,pred3] , [[h, np.tile(UPDOWN, h.shape)], [h,np.tile(UPWARDS, h.shape)], FARAWAY]) + + def get_Normal(self, hit): + # M = intersection point + return (hit.point - self.center) * (1. / self.radius) + + def get_uv(self, hit): + M_C = (hit.point - self.center) / self.radius + phi = np.arctan2(M_C.z, M_C.x) + theta = np.arcsin(M_C.y) + u = (phi + np.pi) / (2*np.pi) + v = (theta + np.pi/2) / np.pi + return u,v \ No newline at end of file diff --git a/Programming/TRAK/sightpy/geometry/surface.py b/Programming/TRAK/sightpy/geometry/surface.py new file mode 100644 index 00000000..600bbb92 --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/surface.py @@ -0,0 +1,30 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3 +import numpy as np + + +class Surface: + def __init__(self, center, material, shadow = True): + self.center = center + self.material = material + self.material.assigned_surface = self + self.shadow = shadow + self.collider_list = [] + + + def rotate(self, θ, u): + + u = u.normalize() + θ = θ/180 *np.pi + cosθ = np.cos(θ) + sinθ = np.sqrt(1-cosθ**2) * np.sign(θ) + + #rotation matrix along u axis + M = np.array([ + [cosθ + u.x*u.x * (1-cosθ), u.x*u.y*(1-cosθ) - u.z*sinθ, u.x*u.z*(1-cosθ) +u.y*sinθ], + [u.y*u.x*(1-cosθ) + u.z*sinθ, cosθ + u.y**2 * (1-cosθ), u.y*u.z*(1-cosθ) -u.x*sinθ], + [u.z*u.x*(1-cosθ) -u.y*sinθ, u.z*u.y*(1-cosθ) + u.x*sinθ, cosθ + u.z*u.z*(1-cosθ)] + ]) + for c in self.collider_list: + c.rotate(M, self.center) + diff --git a/Programming/TRAK/sightpy/geometry/triangle.py b/Programming/TRAK/sightpy/geometry/triangle.py new file mode 100644 index 00000000..bee1cb87 --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/triangle.py @@ -0,0 +1,77 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from ..geometry.primitive import Primitive +from ..geometry.collider import Collider + + +class Triangle(Primitive): + def __init__(self,center, material, p1 , p2, p3, max_ray_depth,shadow = True): + super().__init__(center, material, max_ray_depth, shadow = shadow) + self.collider_list += [Triangle_Collider(assigned_primitive = self, p1 =p1, p2 = p2, p3 = p3)] + + def get_uv(self, M, collider): + return collider.get_uv(M) + + +class Triangle_Collider(Collider): + def __init__(self,assigned_surface, p1, p2, p3): + + + self.assigned_primitive = assigned_surface + self.p1 = p1 + self.p2 = p2 + self.p3 = p3 + self.normal = ((self.p2 - self.p1).cross( self.p3 - self.p1)).normalize() + + + self.centroid = (self.p1 + self.p2 + self.p3)/3 #one possible definition of center. Used for intersect(). + + + self.n31 = (self.p3 - self.p1).cross(self.normal) + self.n12 = (self.p1- self.p2).cross(self.normal) + self.n23 = (self.p2 - self.p3).cross(self.normal) + + + + def intersect(self, O, D): + N = self.normal + + NdotD = N.dot(D) + NdotD = np.where(NdotD == 0., NdotD + 0.0001, NdotD) #avoid zero division + + NdotC_O = N.dot(self.centroid - O) + d = D * NdotC_O / NdotD + M = O + d # intersection point + dis = d.length() + M_C = M - self.centroid + hit_inside = (self.n31.dot(M-self.p1) >= 0) & (self.n12.dot(M-self.p2) >= 0)& (self.n23.dot(M-self.p3) >= 0) & (NdotC_O * NdotD > 0) + hit_UPWARDS = (NdotD < 0) + hit_UPDOWN = np.logical_not(hit_UPWARDS) + + + pred1 = hit_inside & hit_UPWARDS + pred2 = hit_inside & hit_UPDOWN + pred3 = True + return np.select([pred1,pred2,pred3] , [[dis, np.tile(UPWARDS, dis.shape) ], [dis,np.tile(UPDOWN, dis.shape)], FARAWAY]) + + def rotate(self,M, center): + self.p1 = center + (self.p1 -center).matmul(M) + self.p2 = center + (self.p2 -center).matmul(M) + self.p3 = center + (self.p3 -center).matmul(M) + + self.n31 = self.n31.matmul(M) + self.n12 = self.n12.matmul(M) + self.n23 = self.n23.matmul(M) + self.normal = self.normal.matmul(M) + self.centroid = center + (self.centroid-center).matmul(M) + + def get_uv(self, hit): + M_C = hit.point - self.center + u = ((self.pu.dot(M_C)/self.w + 1 ) /2 + self.uv_shift[0]) + v = ((self.pv.dot(M_C)/self.h + 1 ) /2 + self.uv_shift[1]) + return u,v + + + def get_Normal(self, hit): + return self.normal diff --git a/Programming/TRAK/sightpy/geometry/triangle_mesh.py b/Programming/TRAK/sightpy/geometry/triangle_mesh.py new file mode 100644 index 00000000..7e95d55a --- /dev/null +++ b/Programming/TRAK/sightpy/geometry/triangle_mesh.py @@ -0,0 +1,39 @@ +import numpy as np +from ..utils.constants import * +from ..utils.vector3 import vec3 +from ..geometry import Primitive, Triangle_Collider + + +# WORK IN PROGRESS. +# We need to implement a bounding volume hierarchy to make TriangleMesh collision efficient. +# Without a bounding volume hierarchy a model with 200 triangles takes around 3 minutes to be rendered + +class TriangleMesh(Primitive): + def __init__(self,file_name, center, material, max_ray_depth,shadow = True): + super().__init__(center, material,max_ray_depth, shadow = shadow) + self.collider_list += [] + vs = [] + fs = [] + with open(file_name, 'r') as f: + r = f.read() + r = r.split('\n') + for i in r: + i = i.split() + if not i: + continue + elif i[0] == 'v': + x = float(i[1]) + y = float(i[2]) + z = float(i[3]) + vs.append(vec3(x, y, z)) + elif i[0] == 'f': + f1 = int(i[1].split('/')[0]) - 1 + f2 = int(i[2].split('/')[0]) - 1 + f3 = int(i[3].split('/')[0]) - 1 + fs.append([f1, f2, f3]) + for i in fs: + p1 = vs[i[0]] + center + p2 = vs[i[1]] + center + p3 = vs[i[2]] + center + self.collider_list += [colliders.Triangle_Collider(assigned_primitive = self, p1 =p1, p2 = p2, p3 = p3)] + diff --git a/Programming/TRAK/sightpy/lights.py b/Programming/TRAK/sightpy/lights.py new file mode 100644 index 00000000..3335da13 --- /dev/null +++ b/Programming/TRAK/sightpy/lights.py @@ -0,0 +1,49 @@ +from .utils.constants import SKYBOX_DISTANCE +import numpy as np +from abc import abstractmethod + + +# lights only have effect on Glossy materials +class Light: + def __init__(self, pos, color): + self.pos = pos + self.color = color + + @abstractmethod + def get_L(self): + pass + + @abstractmethod + def get_irradiance(self, dist_light, NdotL): + pass + + @abstractmethod + def get_distance(self, M): + pass + + +class PointLight(Light): + def __init__(self, pos, color): + self.pos = pos + self.color = color + def get_L(self): + return (self.pos - M)*(1./(dist_light)) + + def get_distance(self, M): + return np.sqrt((self.pos - M).dot(self.pos - M)) + + def get_irradiance(self,dist_light, NdotL): + return self.color * NdotL/(dist_light**2.) * 100 + +class DirectionalLight(Light): + def __init__(self, Ldir, color): + self.Ldir = Ldir + self.color = color + def get_L(self): + return self.Ldir + + def get_distance(self, M): + return SKYBOX_DISTANCE + + def get_irradiance(self, dist_light, NdotL): + return self.color * NdotL \ No newline at end of file diff --git a/Programming/TRAK/sightpy/materials/__init__.py b/Programming/TRAK/sightpy/materials/__init__.py new file mode 100644 index 00000000..88b13c0b --- /dev/null +++ b/Programming/TRAK/sightpy/materials/__init__.py @@ -0,0 +1,7 @@ +from .material import Material + +from .glossy import Glossy +from .refractive import Refractive +from .thin_film_interference import ThinFilmInterference +from .diffuse import Diffuse +from .emissive import Emissive \ No newline at end of file diff --git a/Programming/TRAK/sightpy/materials/diffuse.py b/Programming/TRAK/sightpy/materials/diffuse.py new file mode 100644 index 00000000..1a11b3bc --- /dev/null +++ b/Programming/TRAK/sightpy/materials/diffuse.py @@ -0,0 +1,99 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3, rgb, extract +from ..utils.random import spherical_caps_pdf, cosine_pdf, mixed_pdf +from functools import reduce as reduce +from ..ray import Ray, get_raycolor +from .. import lights +import numpy as np +from . import Material +from ..textures import * + +class Diffuse(Material): + def __init__(self, diff_color, diffuse_rays = 20, ambient_weight = 0.5, **kwargs): + super().__init__(**kwargs) + + if isinstance(diff_color, vec3): + self.diff_texture = solid_color(diff_color) + elif isinstance(diff_color, texture): + self.diff_texture = diff_color + + self.diffuse_rays = diffuse_rays + self.max_diffuse_reflections = 2 + self.ambient_weight = ambient_weight + + def get_color(self, scene, ray, hit): + + hit.point = (ray.origin + ray.dir * hit.distance) # intersection point + N = hit.material.get_Normal(hit) # normal + + diff_color = self.diff_texture.get_color(hit) + + + color = rgb(0.,0.,0.) + + if ray.diffuse_reflections < 1: + + + nudged = hit.point + N * .000001 + N_repeated = N.repeat(self.diffuse_rays) + + + if ray.n.shape() == 1: + n_repeated = ray.n + else: + n_repeated = ray.n.repeat(self.diffuse_rays) + + nudged_repeated = nudged.repeat(self.diffuse_rays) + hit_repeated = hit.point.repeat(self.diffuse_rays) + + + size = N.shape()[0] * self.diffuse_rays + + pdf1 = cosine_pdf(size, N_repeated) + pdf2 = spherical_caps_pdf(size, nudged_repeated, scene.importance_sampled_list) + + s_pdf = None + if scene.importance_sampled_list == []: + s_pdf = cosine_pdf(size, N_repeated) + else: + s_pdf = mixed_pdf(size, pdf1, pdf2, self.ambient_weight) + + ray_dir = s_pdf.generate() + PDF_val = s_pdf.value(ray_dir) + + NdotL = np.clip(ray_dir.dot(N_repeated),0.,1.) + color_temp = get_raycolor(Ray(nudged_repeated, ray_dir, ray.depth + 1, n_repeated, ray.reflections + 1, ray.transmissions, ray.diffuse_reflections + 1), scene) + color_temp = color_temp * NdotL / PDF_val / (np.pi) # diff_color/np.pi = Lambertian BRDF + color += diff_color * color_temp.reshape(N.shape()[0], self.diffuse_rays).mean(axis = 1) + + return color + + elif ray.diffuse_reflections < self.max_diffuse_reflections: + + """ + when ray.diffuse_reflections > 1 we just call one diffuse ray to solve rendering equation (otherwise is too slow) + """ + + nudged = hit.point + N * .000001 + size = N.shape()[0] + s_pdf = None + + pdf1 = cosine_pdf(size, N) + pdf2 = spherical_caps_pdf(size, nudged, scene.importance_sampled_list) + + if scene.importance_sampled_list == []: + s_pdf = cosine_pdf(size, N) + else: + s_pdf = mixed_pdf(size, pdf1, pdf2, self.ambient_weight) + + ray_dir = s_pdf.generate() + PDF_val = s_pdf.value(ray_dir) + + NdotL = np.clip(N.dot(ray_dir),0.,1.) + color_temp = diff_color * get_raycolor(Ray(nudged, ray_dir, ray.depth + 1, ray.n, ray.reflections + 1, ray.transmissions, ray.diffuse_reflections + 1), scene) + color = color_temp * NdotL / PDF_val / (np.pi) + + return color + + else: + return color diff --git a/Programming/TRAK/sightpy/materials/emissive.py b/Programming/TRAK/sightpy/materials/emissive.py new file mode 100644 index 00000000..3f375c64 --- /dev/null +++ b/Programming/TRAK/sightpy/materials/emissive.py @@ -0,0 +1,23 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3, rgb, extract +from functools import reduce as reduce +from ..ray import Ray, get_raycolor +from .. import lights +import numpy as np +from . import Material +from ..textures import * + +class Emissive(Material): + def __init__(self, color, **kwargs): + + if isinstance(color, vec3): + self.texture_color = solid_color(color) + elif isinstance(color, texture): + self.texture_color = color + + super().__init__(**kwargs) + + + def get_color(self, scene, ray, hit): + diff_color = self.texture_color.get_color(hit) + return diff_color \ No newline at end of file diff --git a/Programming/TRAK/sightpy/materials/glossy.py b/Programming/TRAK/sightpy/materials/glossy.py new file mode 100644 index 00000000..98162167 --- /dev/null +++ b/Programming/TRAK/sightpy/materials/glossy.py @@ -0,0 +1,91 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3, rgb, extract +from functools import reduce as reduce +from ..ray import Ray, get_raycolor +from .. import lights +import numpy as np +from . import Material +from ..textures import * + +class Glossy(Material): + def __init__(self, diff_color, roughness, spec_coeff, diff_coeff, n, **kwargs): + super().__init__(**kwargs) + + if isinstance(diff_color, vec3): + self.diff_texture = solid_color(diff_color) + elif isinstance(diff_color, texture): + self.diff_texture = diff_color + + self.roughness = roughness + self.diff_coeff = diff_coeff + self.spec_coeff = spec_coeff + self.n = n # index of refraction + + + def get_color(self, scene, ray, hit): + + hit.point = (ray.origin + ray.dir * hit.distance) # intersection point + N = hit.material.get_Normal(hit) # normal + + diff_color = self.diff_texture.get_color(hit)* self.diff_coeff + + # Ambient + color = scene.ambient_color * diff_color + V = ray.dir*-1. + nudged = hit.point + N * .000001 # M nudged to avoid itself + + for light in scene.Light_list: + + L = light.get_L() # direction to light + dist_light = light.get_distance(hit.point) # distance to light + NdotL = np.maximum(N.dot(L), 0.) + lv = light.get_irradiance(dist_light, NdotL) # amount of intensity that falls on the surface + + + # direction to ray origin + + H = (L + V).normalize() # Half-way vector + + + + + # Shadow: find if the point is shadowed or not. + # This amounts to finding out if M can see the light + # Shoot a ray from M to L and check what object is the nearest + if not scene.shadowed_collider_list == []: + inters = [s.intersect(nudged, L) for s in scene.shadowed_collider_list] + light_distances, light_hit_orientation = zip(*inters) + light_nearest = reduce(np.minimum, light_distances) + seelight = (light_nearest >= dist_light) + else: + seelight = 1. + + # Lambert shading (diffuse) + color += diff_color * lv * seelight + + if self.roughness != 0.0: + #Fresnel Factor for specular highlight (Schlick’s approximation) + F0 = np.abs((ray.n - self.n)/(ray.n + self.n))**2 + cosθ = np.clip(V.dot(H), 0.0, 1.) + F = F0 + (1. - F0)*(1.- cosθ)**5 + + + # Phong shading (specular highlight) + a = 2./(self.roughness**2.) - 2. + Dphong = np.power(np.clip(N.dot(H), 0., 1.), a) * (a + 2.)/(2.*np.pi) + + # Cook-Torrance model + color += F * Dphong /(4. * np.clip(N.dot(V) * NdotL, 0.001, 1.) ) * seelight * lv * self.spec_coeff + + # Reflection + if ray.depth < hit.surface.max_ray_depth: + + # Fresnel Factor for reflections (Schlick’s approximation) + + F0 = np.abs((scene.n - self.n)/(scene.n + self.n))**2 + cosθ = np.clip(V.dot(N),0.0,1.) + F = F0 + (1. - F0)*(1.- cosθ)**5 + reflected_ray_dir = (ray.dir - N * 2. * ray.dir.dot(N)).normalize() + color += (get_raycolor(Ray(nudged, reflected_ray_dir, ray.depth + 1, ray.n, ray.reflections + 1, ray.transmissions, ray.diffuse_reflections), scene))*F + + return color \ No newline at end of file diff --git a/Programming/TRAK/sightpy/materials/material.py b/Programming/TRAK/sightpy/materials/material.py new file mode 100644 index 00000000..ef6b050c --- /dev/null +++ b/Programming/TRAK/sightpy/materials/material.py @@ -0,0 +1,34 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3, rgb, extract +from functools import reduce as reduce +from ..ray import Ray, get_raycolor +from .. import lights +from ..utils.image_functions import load_image, load_image_as_linear_sRGB +import numpy as np +from abc import abstractmethod + +class Material(): + def __init__(self,normalmap = None): + + if normalmap != None: + normalmap = load_image("sightpy/normalmaps/" + normalmap) + self.normalmap = normalmap + + + def get_Normal(self, hit): + N_coll = hit.collider.get_Normal(hit) + if self.normalmap is not None: + u,v = hit.get_uv() + im = self.normalmap[-((v * self.normalmap.shape[0]*self.repeat ).astype(int)% self.normalmap.shape[0]) , (u * self.normalmap.shape[1]*self.repeat).astype(int) % self.normalmap.shape[1] ].T + N_map = (vec3(im[0] - 0.5,im[1] - 0.5,im[2] - 0.5)) * 2.0 + return N_map.matmul(hit.collider.inverse_basis_matrix).normalize()*hit.orientation + else: + return N_coll*hit.orientation + + def set_normalmap(self, normalmap,repeat= 1.0): + self.normalmap = load_image("sightpy/normalmaps/" + normalmap) + self.repeat = repeat + + @abstractmethod + def get_color(self, scene, ray, hit): + pass diff --git a/Programming/TRAK/sightpy/materials/refractive.py b/Programming/TRAK/sightpy/materials/refractive.py new file mode 100644 index 00000000..c4524258 --- /dev/null +++ b/Programming/TRAK/sightpy/materials/refractive.py @@ -0,0 +1,85 @@ +from ..utils.constants import * +from ..utils.vector3 import vec3, rgb, extract +from functools import reduce as reduce +from ..ray import Ray, get_raycolor +from .. import lights +import numpy as np +from . import Material + +class Refractive(Material): + def __init__(self, n, **kwargs): + super().__init__(**kwargs) + + self.n = n # index of refraction + + # Instead of defining a index of refraction (n) for each wavelenght (computationally expensive) we aproximate defining the index of refraction + # using a vec3 for red = 630 nm, green 555 nm, blue 475 nm, the most sensitive wavelenghts of human eye. + + # Index a refraction is a complex number. + # The real part is involved in how much light is reflected and model refraction direction via Snell Law. + # The imaginary part of n is involved in how much light is reflected and absorbed. For non-transparent materials like metals is usually between (0.1j,3j) + # and for transparent materials like glass is usually between (0.j , 1e-7j) + + + + def get_color(self, scene, ray, hit): + + hit.point = (ray.origin + ray.dir * hit.distance) # intersection point + N = hit.material.get_Normal(hit) # normal + + color = rgb(0.,0.,0.) + + V = ray.dir*-1. # direction to ray origin + nudged = hit.point + N * .000001 # M nudged to avoid itself + # compute reflection and refraction + # a paper explaining formulas used: + # https://graphics.stanford.edu/courses/cs148-10-summer/docs/2006--degreve--reflection_refraction.pdf + # reallistic refraction is expensive. (requires exponential complexity because each ray is divided in two) + + if ray.depth 1.0) by scaling + rgb_max = np.amax(rgb, axis=0) + 0.00001 # avoid division by zero + intensity_cutoff = 1.0 + rgb = np.where(rgb_max > intensity_cutoff, rgb * intensity_cutoff / (rgb_max), rgb) + + return rgb + + +def sRGB_to_sRGB_linear(rgb): + + '''sRGB standard for gamma inverse correction.''' + rgb_linear = np.where( rgb <= 0.03928, rgb / 12.92, np.power((rgb + 0.055) / 1.055, 2.4)) + + return rgb_linear + diff --git a/Programming/TRAK/sightpy/utils/constants.py b/Programming/TRAK/sightpy/utils/constants.py new file mode 100644 index 00000000..0c39f367 --- /dev/null +++ b/Programming/TRAK/sightpy/utils/constants.py @@ -0,0 +1,5 @@ +UPWARDS = 1 +UPDOWN = -1 +FARAWAY = 1.0e39 +SKYBOX_DISTANCE = 1.0e6 + diff --git a/Programming/TRAK/sightpy/utils/image_functions.py b/Programming/TRAK/sightpy/utils/image_functions.py new file mode 100644 index 00000000..34ea7374 --- /dev/null +++ b/Programming/TRAK/sightpy/utils/image_functions.py @@ -0,0 +1,40 @@ +from PIL import Image, ImageFilter +import numpy as np +from pathlib import Path +from .colour_functions import sRGB_to_sRGB_linear + + +def load_image(path): + img = Image.open(Path(path)) + return np.asarray(img)/256. + + +def load_image_with_blur(path, blur = 0.): + img = Image.open(Path(path)) + img = img.filter(ImageFilter.GaussianBlur(radius=blur)) + + return np.asarray(img)/256. + + + + +def load_image_as_linear_sRGB(path, blur = 0.0): + + path = Path(path) + location = str(path.parents[0]) + name = str(path.name)#be sure that image doesn't lose quality + + print("proccesing " + name) + img = Image.open(path) + + if blur != 0.0: + img = img.filter(ImageFilter.GaussianBlur(radius=blur)) + + img_array = np.asarray(img)/256. + img_sRGB_linear_array = sRGB_to_sRGB_linear(img_array) + return img_sRGB_linear_array + + + + + diff --git a/Programming/TRAK/sightpy/utils/random.py b/Programming/TRAK/sightpy/utils/random.py new file mode 100644 index 00000000..0166fc8b --- /dev/null +++ b/Programming/TRAK/sightpy/utils/random.py @@ -0,0 +1,231 @@ +import numpy as np +from ..utils.vector3 import vec3 +from abc import abstractmethod + +def random_in_unit_disk(shape): + r = np.sqrt(np.random.rand(shape)) + phi = np.random.rand(shape)*2*np.pi + return r * np.cos(phi), r * np.sin(phi) + +def random_in_unit_sphere(shape): + + #https://mathworld.wolfram.com/SpherePointPicking.html + phi = np.random.rand(shape)*2*np.pi + u = 2.*np.random.rand(shape) - 1. + r = np.sqrt(1-u**2) + return vec3( r*np.cos(phi), r*np.sin(phi), u) + + + +class PDF: + """Probability density function""" + @abstractmethod + def value(self,ray_dir): + """get probability density function value at direction ray_dir""" + pass + + @abstractmethod + def generate(self): + """generate random ray directions according the probability density function""" + pass + + + + +class hemisphere_pdf(PDF): + """Probability density Function""" + def __init__(self,shape, normal): + self.shape = shape + self.normal = normal + + + def value(self,ray_dir): + return 1./(2.*np.pi) + + def generate(self): + r = random_in_unit_sphere(self.shape) + return vec3.where( self.normal.dot(r) < 0. , r*-1., r ) + + +class cosine_pdf(PDF): + """Probability density Function""" + def __init__(self,shape, normal): + self.shape = shape + self.normal = normal + + + + def value(self,ray_dir): + return np.clip(ray_dir.dot(self.normal),0.,1.)/np.pi + + def generate(self): + ax_w = self.normal + a = vec3.where( np.abs(ax_w.x) > 0.9 , vec3(0,1,0) , vec3(1,0,0)) + ax_v = ax_w.cross(a).normalize() + ax_u = ax_w.cross(ax_v) + + phi = np.random.rand(self.shape)*2*np.pi + r2 = np.random.rand(self.shape) + + z = np.sqrt(1 - r2) + x = np.cos(phi) * np.sqrt(r2) + y = np.sin(phi) * np.sqrt(r2) + + return ax_u*x + ax_v*y + ax_w*z + + + + +class spherical_caps_pdf(PDF): + """Probability density Function""" + def __init__(self,shape, origin, importance_sampled_list): + self.shape = shape + self.origin = origin + self.importance_sampled_list = importance_sampled_list + self.l = len(importance_sampled_list) + + def value(self, ray_dir): + PDF_value = 0. + for i in range(self.l): + PDF_value += np.where( ray_dir.dot(self.ax_w_list[i]) > self.cosθmax_list[i] , 1/((1 - self.cosθmax_list[i])*2*np.pi) , 0. ) + PDF_value = PDF_value/self.l + return PDF_value + + + def generate(self): + shape = self.shape + origin = self.origin + importance_sampled_list = self.importance_sampled_list + l = self.l + + mask = (np.random.rand(shape) * l).astype(int) + mask_list = [None]*l + + cosθmax_list = [None]*l + ax_u_list = [None]*l + ax_v_list = [None]*l + ax_w_list = [None]*l + + for i in range(l): + + ax_w_list[i] = (importance_sampled_list[i].center - origin).normalize() + a = vec3.where( np.abs(ax_w_list[i].x) > 0.9 , vec3(0,1,0) , vec3(1,0,0)) + ax_v_list[i] = ax_w_list[i].cross(a).normalize() + ax_u_list[i] = ax_w_list[i].cross(ax_v_list[i]) + mask_list[i] = mask == i + + + target_distance = np.sqrt((importance_sampled_list[i].center - origin).dot(importance_sampled_list[i].center - origin)) + + cosθmax_list[i] = np.sqrt(1 - np.clip(importance_sampled_list[i].bounded_sphere_radius / target_distance, 0., 1.)**2 ) + + self.cosθmax_list = cosθmax_list + self.ax_w_list = ax_w_list + + phi = np.random.rand(shape)*2*np.pi + r2 = np.random.rand(shape) + + cosθmax = np.select(mask_list, cosθmax_list) + ax_w = vec3.select(mask_list, ax_w_list) + ax_v = vec3.select(mask_list, ax_v_list) + ax_u = vec3.select(mask_list, ax_u_list) + + z = 1. + r2 * (cosθmax - 1.) + x = np.cos(phi) * np.sqrt(1. - z**2) + y = np.sin(phi) * np.sqrt(1. - z**2) + + ray_dir = ax_u*x + ax_v*y + ax_w*z + return ray_dir + + +class mixed_pdf(PDF): + """Probability density Function""" + def __init__(self,shape, pdf1, pdf2, pdf1_weight = 0.5): + + self.pdf1_weight = pdf1_weight + self.pdf2_weight = 1. - pdf1_weight + self.shape = shape + self.pdf1 = pdf1 + self.pdf2 = pdf2 + + + def value(self,ray_dir): + return self.pdf1.value(ray_dir) * self.pdf1_weight + self.pdf2.value(ray_dir) * self.pdf2_weight + + def generate(self): + mask = np.random.rand(self.shape) + return vec3.where( mask < self.pdf1_weight, self.pdf1.generate(), self.pdf2.generate() ) + + + + + + + +def random_in_unit_spherical_caps(shape, origin, importance_sampled_list): + + l = len(importance_sampled_list) + + + mask = (np.random.rand(shape) * l).astype(int) + mask_list = [None]*l + + cosθmax_list = [None]*l + ax_u_list = [None]*l + ax_v_list = [None]*l + ax_w_list = [None]*l + + for i in range(l): + + ax_w_list[i] = (importance_sampled_list[i].center - origin).normalize() + a = vec3.where( np.abs(ax_w_list[i].x) > 0.9 , vec3(0,1,0) , vec3(1,0,0)) + ax_v_list[i] = ax_w_list[i].cross(a).normalize() + ax_u_list[i] = ax_w_list[i].cross(ax_v_list[i]) + mask_list[i] = mask == i + + + target_distance = np.sqrt((importance_sampled_list[i].center - origin).dot(importance_sampled_list[i].center - origin)) + + cosθmax_list[i] = np.sqrt(1 - np.clip(importance_sampled_list[i].bounded_sphere_radius / target_distance, 0., 1.)**2 ) + + + phi = np.random.rand(shape)*2*np.pi + r2 = np.random.rand(shape) + + cosθmax = np.select(mask_list, cosθmax_list) + ax_w = vec3.select(mask_list, ax_w_list) + ax_v = vec3.select(mask_list, ax_v_list) + ax_u = vec3.select(mask_list, ax_u_list) + + z = 1. + r2 * (cosθmax - 1.) + x = np.cos(phi) * np.sqrt(1. - z**2) + y = np.sin(phi) * np.sqrt(1. - z**2) + + ray_dir = ax_u*x + ax_v*y + ax_w*z + + PDF = 0. + for i in range(l): + PDF += np.where( ray_dir.dot(ax_w_list[i]) > cosθmax_list[i] , 1/((1 - cosθmax_list[i])*2*np.pi) , 0. ) + PDF = PDF/l + + return ray_dir, PDF + +def random_in_unit_spherical_cap(shape,cosθmax,normal): + + + ax_w = normal + a = vec3.where( np.abs(ax_w.x) > 0.9 , vec3(0,1,0) , vec3(1,0,0)) + ax_v = ax_w.cross(a).normalize() + ax_u = ax_w.cross(ax_v) + + phi = np.random.rand(shape)*2*np.pi + r2 = np.random.rand(shape) + + z = 1. + r2 * (cosθmax - 1.) + x = np.cos(phi) * np.sqrt(1. - z**2) + y = np.sin(phi) * np.sqrt(1. - z**2) + + + + + return ax_u*x + ax_v*y + ax_w*z \ No newline at end of file diff --git a/Programming/TRAK/sightpy/utils/vector3.py b/Programming/TRAK/sightpy/utils/vector3.py new file mode 100644 index 00000000..43294adb --- /dev/null +++ b/Programming/TRAK/sightpy/utils/vector3.py @@ -0,0 +1,188 @@ +import numpy as np +import numbers + +def extract(cond, x): + if isinstance(x, numbers.Number): + return x + else: + return np.extract(cond, x) + +class vec3(): + + def __init__(self, x, y, z): + self.x = x + self.y = y + self.z = z + + def __str__(self): + # Used for debugging. This method is called when you print an instance + return "(" + str(self.x) + ", " + str(self.y) + ", " + str(self.z) + ")" + + + def __add__(self, v): + if isinstance(v, vec3): + return vec3(self.x + v.x, self.y + v.y, self.z + v.z) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(self.x + v, self.y + v, self.z + v) + def __radd__(self, v): + if isinstance(v, vec3): + return vec3(self.x + v.x, self.y + v.y, self.z + v.z) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(self.x + v, self.y + v, self.z + v) + def __sub__(self, v): + if isinstance(v, vec3): + return vec3(self.x - v.x, self.y - v.y, self.z - v.z) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(self.x - v, self.y - v, self.z - v) + def __rsub__(self, v): + if isinstance(v, vec3): + return vec3(v.x - self.x, v.y - self.y , v.z - self.z) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(v - self.x, v - self.y , v - self.z) + + def __mul__(self, v): + if isinstance(v, vec3): + return vec3(self.x * v.x , self.y * v.y , self.z * v.z ) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(self.x * v, self.y * v, self.z * v) + def __rmul__(self, v): + if isinstance(v, vec3): + return vec3(v.x *self.x , v.y * self.y, v.z * self.z ) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(v *self.x , v * self.y, v * self.z ) + def __truediv__(self, v): + if isinstance(v, vec3): + return vec3(self.x / v.x , self.y / v.y , self.z / v.z ) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(self.x / v, self.y / v, self.z / v) + + + def __rtruediv__(self, v): + if isinstance(v, vec3): + return vec3(v.x / self.x, v.y / self.y, v.z / self.z) + elif isinstance(v, numbers.Number) or isinstance(v, np.ndarray): + return vec3(v / self.x, v / self.y, v / self.z) + + + + def __abs__(self): + return vec3(np.abs(self.x), np.abs(self.y), np.abs(self.z)) + + def real(v): + return vec3(np.real(v.x), np.real(v.y), np.real(v.z)) + + def imag(v): + return vec3(np.imag(v.x), np.imag(v.y), np.imag(v.z)) + + def yzx(self): + return vec3(self.y, self.z, self.x) + def xyz(self): + return vec3(self.x, self.y, self.z) + def zxy(self): + return vec3(self.z, self.x, self.y) + def xyz(self): + return vec3(self.x, self.y, self.z) + + + def average(self): + return (self.x + self.y + self.z)/3 + + def matmul(self, matrix): + if isinstance(self.x, numbers.Number): + return array_to_vec3(np.dot(matrix,self.to_array())) + elif isinstance(self.x, np.ndarray): + return array_to_vec3(np.tensordot(matrix,self.to_array() , axes=([1,0]))) + + def change_basis(self, new_basis): + return vec3(self.dot(new_basis[0]), self.dot(new_basis[1]), self.dot(new_basis[2])) + + def __pow__(self, a): + return vec3(self.x**a, self.y**a, self.z**a) + + def dot(self, v): + return self.x*v.x + self.y*v.y + self.z*v.z + + def exp(v): + return vec3(np.exp(v.x) , np.exp(v.y) ,np.exp(v.z)) + + def sqrt(v): + return vec3(np.sqrt(v.x) , np.sqrt(v.y) ,np.sqrt(v.z)) + + def to_array(self): + return np.array([self.x , self.y , self.z]) + + def cross(self, v): + return vec3(self.y*v.z - self.z*v.y, -self.x*v.z + self.z*v.x, self.x*v.y - self.y*v.x) + + def length(self): + return np.sqrt(self.dot(self)) + + def square_length(self): + return self.dot(self) + + def normalize(self): + mag = self.length() + return self * (1.0 / np.where(mag == 0, 1, mag)) + + + + def components(self): + return (self.x, self.y, self.z) + + def extract(self, cond): + return vec3(extract(cond, self.x), + extract(cond, self.y), + extract(cond, self.z)) + + def where(cond, out_true, out_false): + return vec3(np.where(cond, out_true.x, out_false.x), + np.where(cond, out_true.y, out_false.y), + np.where(cond, out_true.z, out_false.z)) + + def select(mask_list, out_list): + out_list_x = [i.x for i in out_list] + out_list_y = [i.y for i in out_list] + out_list_z = [i.z for i in out_list] + + return vec3(np.select(mask_list, out_list_x), + np.select(mask_list, out_list_y), + np.select(mask_list, out_list_z)) + + def clip(self, min, max): + return vec3(np.clip(self.x, min, max), + np.clip(self.y, min, max), + np.clip(self.z, min, max)) + + def place(self, cond): + r = vec3(np.zeros(cond.shape), np.zeros(cond.shape), np.zeros(cond.shape)) + np.place(r.x, cond, self.x) + np.place(r.y, cond, self.y) + np.place(r.z, cond, self.z) + return r + + def repeat(self, n): + return vec3(np.repeat(self.x , n), np.repeat(self.y , n), np.repeat(self.z , n)) + + def reshape(self, *newshape): + return vec3(self.x.reshape(*newshape), self.y.reshape(*newshape), self.z.reshape(*newshape)) + + def shape(self, *newshape): + if isinstance(self.x, numbers.Number): + return 1 + elif isinstance(self.x, np.ndarray): + return self.x.shape + + def mean(self, axis): + return vec3(np.mean(self.x,axis = axis), np.mean(self.y,axis = axis), np.mean(self.z,axis = axis)) + + def __eq__(self, other): + return (self.x == other.x) & (self.y == other.y) & (self.z == other.z) + + +def array_to_vec3(array): + return vec3(array[0],array[1],array[2]) + + + +global rgb +rgb = vec3 \ No newline at end of file diff --git a/Programming/TRAK/utils.py b/Programming/TRAK/utils.py new file mode 100644 index 00000000..b211d68c --- /dev/null +++ b/Programming/TRAK/utils.py @@ -0,0 +1,13 @@ +from configparser import ConfigParser + +def load_config(config_path): + config = ConfigParser() + config.read(config_path) + return config + +def parse_resolution(resolution): + try: + width, height = map(int, resolution.lower().split('x')) + return width, height + except ValueError: + raise ValueError("Resolution must be in the format WIDTHxHEIGHT, e.g., 1920x1080.") \ No newline at end of file