Add 'Programming/TRAK/' from commit '777937fb9ee618a73e357f5b25abe00744a4ab99'
git-subtree-dir: Programming/TRAK git-subtree-mainline:e11d703c3egit-subtree-split:777937fb9e
166
Programming/TRAK/.gitignore
vendored
Normal file
@ -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
|
||||
8
Programming/TRAK/.idea/.gitignore
vendored
Normal file
@ -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
|
||||
10
Programming/TRAK/.idea/TRAK.iml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.8 (TRAK)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
Programming/TRAK/.idea/misc.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.8 (TRAK)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (TRAK)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
Programming/TRAK/.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/TRAK.iml" filepath="$PROJECT_DIR$/.idea/TRAK.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
Programming/TRAK/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
Programming/TRAK/.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.11.11
|
||||
7
Programming/TRAK/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.autopep8",
|
||||
"ms-python.pylint"
|
||||
]
|
||||
}
|
||||
23
Programming/TRAK/README.md
Normal file
@ -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
|
||||
```
|
||||
2
Programming/TRAK/code/mapa_srodowiska/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
Download file in (!) EXR (!) format
|
||||
https://polyhaven.com/a/lilienstein
|
||||
BIN
Programming/TRAK/code/mapa_srodowiska/lilienstein_1k.exr
Normal file
162
Programming/TRAK/code/mapa_srodowiska/main.py
Normal file
@ -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)
|
||||
3
Programming/TRAK/code/photonmapping/cpp/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
photon_mapping
|
||||
*.ppm
|
||||
*.jpg
|
||||
27
Programming/TRAK/code/photonmapping/cpp/Plane.h
Normal file
@ -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
|
||||
31
Programming/TRAK/code/photonmapping/cpp/Sphere.h
Normal file
@ -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
|
||||
23
Programming/TRAK/code/photonmapping/cpp/Vector3.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef VECTOR3_H
|
||||
#define VECTOR3_H
|
||||
|
||||
#include <cmath>
|
||||
|
||||
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
|
||||
1
Programming/TRAK/code/photonmapping/cpp/compile.sh
Executable file
@ -0,0 +1 @@
|
||||
g++ -O2 main.cpp -o photon_mapping
|
||||
296
Programming/TRAK/code/photonmapping/cpp/main.cpp
Normal file
@ -0,0 +1,296 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <fstream>
|
||||
#include <fstream>
|
||||
#include <chrono>
|
||||
#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<double> 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> photon_map;
|
||||
std::vector<void*> objects; // Pointers to objects
|
||||
std::vector<int> 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<Vector3> 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<int>(std::pow(pixel.x, gamma) * 255);
|
||||
int g = static_cast<int>(std::pow(pixel.y, gamma) * 255);
|
||||
int b = static_cast<int>(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<double> 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<double>::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<Sphere*>(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<Plane*>(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<double>::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<Sphere*>(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<Plane*>(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<Sphere*>(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<Plane*>(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
|
||||
}
|
||||
235
Programming/TRAK/code/photonmapping/python/main.py
Normal file
@ -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()
|
||||
16
Programming/TRAK/config.ini
Normal file
@ -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
|
||||
BIN
Programming/TRAK/docs/TRAK___Projekty.pdf
Normal file
3
Programming/TRAK/environments/README.md
Normal file
@ -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.
|
||||
BIN
Programming/TRAK/environments/lake.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
Programming/TRAK/environments/miramar.jpeg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
Programming/TRAK/environments/stormydays.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
76
Programming/TRAK/main.py
Normal file
@ -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()
|
||||
BIN
Programming/TRAK/output_ray_traced.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
Programming/TRAK/outputs/output.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_cornell_box.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_cornell_box_200_200x200.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_cornell_box_200x200.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_transparent_cuboid.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 167 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_two_spheres.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_two_spheres_200_200x200.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
Programming/TRAK/outputs/ray_tracing_two_spheres_200x200.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
191
Programming/TRAK/photon_mapping.py
Normal file
@ -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
|
||||
BIN
Programming/TRAK/ray tracing testy.pdf
Normal file
425
Programming/TRAK/rendering.py
Normal file
@ -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}")
|
||||
11
Programming/TRAK/requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
glfw
|
||||
numpy
|
||||
pyrr
|
||||
PyOpenGL
|
||||
matplotlib
|
||||
flake8
|
||||
black
|
||||
autopep8
|
||||
flake8-max-function-length
|
||||
pillow
|
||||
progressbar
|
||||
3
Programming/TRAK/scenes/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Scene directory
|
||||
|
||||
This is scene directory. Put scene files here.
|
||||
0
Programming/TRAK/scenes/__init__.py
Normal file
118
Programming/TRAK/scenes/cornell_box.py
Normal file
@ -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
|
||||
19
Programming/TRAK/scenes/soap_bubble.py
Normal file
@ -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
|
||||
35
Programming/TRAK/scenes/three_spheres.py
Normal file
@ -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
|
||||
27
Programming/TRAK/scenes/transparent_cuboid.py
Normal file
@ -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
|
||||
38
Programming/TRAK/scenes/two_spheres.py
Normal file
@ -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
|
||||
17
Programming/TRAK/sightpy/__init__.py
Normal file
@ -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 *
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
54
Programming/TRAK/sightpy/animation.py
Normal file
@ -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()
|
||||
0
Programming/TRAK/sightpy/backgrounds/__init__.py
Normal file
BIN
Programming/TRAK/sightpy/backgrounds/lake.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
Programming/TRAK/sightpy/backgrounds/lightmaps/lake.png
Normal file
|
After Width: | Height: | Size: 666 KiB |
BIN
Programming/TRAK/sightpy/backgrounds/miramar.jpeg
Normal file
|
After Width: | Height: | Size: 155 KiB |
18
Programming/TRAK/sightpy/backgrounds/panorama.py
Normal file
@ -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)
|
||||
57
Programming/TRAK/sightpy/backgrounds/skybox.py
Normal file
@ -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)
|
||||
|
||||
BIN
Programming/TRAK/sightpy/backgrounds/stormydays.png
Normal file
|
After Width: | Height: | Size: 614 KiB |
158
Programming/TRAK/sightpy/backgrounds/util/blur_background.py
Normal file
@ -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)
|
||||
|
||||
52
Programming/TRAK/sightpy/camera.py
Normal file
@ -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)
|
||||
|
||||
13
Programming/TRAK/sightpy/geometry/__init__.py
Normal file
@ -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 *
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
17
Programming/TRAK/sightpy/geometry/collider.py
Normal file
@ -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
|
||||
145
Programming/TRAK/sightpy/geometry/cuboid.py
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
81
Programming/TRAK/sightpy/geometry/plane.py
Normal file
@ -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
|
||||
|
||||
30
Programming/TRAK/sightpy/geometry/primitive.py
Normal file
@ -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)
|
||||
|
||||
52
Programming/TRAK/sightpy/geometry/sphere.py
Normal file
@ -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
|
||||
30
Programming/TRAK/sightpy/geometry/surface.py
Normal file
@ -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)
|
||||
|
||||
77
Programming/TRAK/sightpy/geometry/triangle.py
Normal file
@ -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
|
||||
39
Programming/TRAK/sightpy/geometry/triangle_mesh.py
Normal file
@ -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)]
|
||||
|
||||
49
Programming/TRAK/sightpy/lights.py
Normal file
@ -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
|
||||
7
Programming/TRAK/sightpy/materials/__init__.py
Normal file
@ -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
|
||||
99
Programming/TRAK/sightpy/materials/diffuse.py
Normal file
@ -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
|
||||
23
Programming/TRAK/sightpy/materials/emissive.py
Normal file
@ -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
|
||||
91
Programming/TRAK/sightpy/materials/glossy.py
Normal file
@ -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
|
||||
34
Programming/TRAK/sightpy/materials/material.py
Normal file
@ -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
|
||||
85
Programming/TRAK/sightpy/materials/refractive.py
Normal file
@ -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 <hit.surface.max_ray_depth:
|
||||
|
||||
"""
|
||||
if hit_orientation== UPWARDS:
|
||||
#ray enter in the material
|
||||
if hit_orientation== UPDOWN:
|
||||
#ray get out of the material
|
||||
"""
|
||||
n1 = ray.n
|
||||
n2 = vec3.where(hit.orientation== UPWARDS,self.n,scene.n)
|
||||
|
||||
n1_div_n2 = vec3.real(n1)/vec3.real(n2)
|
||||
cosθi = V.dot(N)
|
||||
sin2θt = (n1_div_n2)**2 * (1.-cosθi**2)
|
||||
|
||||
# compute complete fresnel term
|
||||
cosθt = vec3.sqrt(1. - (n1/n2)**2 * (1.-cosθi**2) )
|
||||
r_per = (n1*cosθi - n2*cosθt)/(n1*cosθi + n2*cosθt)
|
||||
r_par = -1.*(n1*cosθt - n2*cosθi)/(n1*cosθt + n2*cosθi)
|
||||
F = (np.abs(r_per)**2 + np.abs(r_par)**2)/2.
|
||||
|
||||
# compute reflection
|
||||
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
|
||||
|
||||
|
||||
|
||||
# compute refraction
|
||||
# Spectrum dispersion is not implemented.
|
||||
# We approximate refraction direction averaging index of refraction of each wavelenght
|
||||
n1_div_n2_aver = n1_div_n2.average()
|
||||
sin2θt = (n1_div_n2_aver)**2 * (1.-cosθi**2)
|
||||
|
||||
non_TiR = (sin2θt <= 1.)
|
||||
if np.any(non_TiR): # avoid total internal reflection
|
||||
|
||||
refracted_ray_dir = (ray.dir*(n1_div_n2_aver) + N*(n1_div_n2_aver * cosθi - np.sqrt(1-np.clip(sin2θt,0,1)))).normalize()
|
||||
nudged = hit.point - N * .000001 #nudged for refraction
|
||||
T = 1. - F
|
||||
refracted_color = (get_raycolor( Ray(nudged, refracted_ray_dir, ray.depth + 1, n2, ray.reflections, ray.transmissions + 1, ray.diffuse_reflections).extract(non_TiR), scene) )*T.extract(non_TiR)
|
||||
color += refracted_color.place(non_TiR)
|
||||
|
||||
|
||||
# absorption:
|
||||
# approximation using wavelength for red = 630 nm, green 550 nm, blue 475 nm
|
||||
color = color *vec3.exp(-2.*vec3.imag(ray.n)*2.*np.pi/vec3(630,550,475) * 1e9* hit.distance)
|
||||
return color
|
||||
67
Programming/TRAK/sightpy/materials/thin_film_interference.py
Normal file
@ -0,0 +1,67 @@
|
||||
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 ..utils.image_functions import load_image
|
||||
|
||||
class ThinFilmInterference(Material):
|
||||
def __init__(self, thickness, noise = 0.,**kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.thickness = thickness
|
||||
|
||||
# precomputed reflectance vs cosθI (vertical axis) and thickness (horizontal axis)
|
||||
self.thin_film_interference_reflectance = load_image("sightpy/textures/thin_film_interference_n=1.4.png")
|
||||
self.thickness_noise = load_image("sightpy/textures/noise.png")
|
||||
self.thickness_noise = (self.thickness_noise[:,:,0])
|
||||
self.noise_factor = noise
|
||||
|
||||
def get_color(self, scene, ray, hit):
|
||||
|
||||
hit.point = (ray.origin + ray.dir * hit.distance) # intersection point
|
||||
N = hit.material.get_Normal(hit) # normal
|
||||
|
||||
# Ambient
|
||||
color = rgb(0.,0.,0.)
|
||||
|
||||
V = ray.dir*-1. # direction to ray origin
|
||||
|
||||
|
||||
if ray.depth < hit.surface.max_ray_depth:
|
||||
|
||||
"""
|
||||
if hit_orientation== UPWARDS:
|
||||
#ray enter in the material
|
||||
if hit_orientation== UPDOWN:
|
||||
#ray get out of the material
|
||||
"""
|
||||
|
||||
cosθi = V.dot(N)
|
||||
|
||||
u,v = hit.get_uv()
|
||||
thickness = self.thickness
|
||||
if self.noise_factor != 0.:
|
||||
thickness += self.noise_factor*(self.thickness_noise[-((v * self.thickness_noise.shape[0]*0.5 ).astype(int)% self.thickness_noise.shape[0]) , (u * self.thickness_noise.shape[1]*0.5).astype(int) % self.thickness_noise.shape[1] ].T - 0.5)
|
||||
Fim = self.thin_film_interference_reflectance[(cosθi* self.thin_film_interference_reflectance.shape[0]).astype(int), thickness.astype(int) ]
|
||||
else:
|
||||
Fim = self.thin_film_interference_reflectance[(cosθi* self.thin_film_interference_reflectance.shape[0]).astype(int), int(thickness) ]
|
||||
|
||||
F = vec3(Fim[:,0],Fim[:,1],Fim[:,2])
|
||||
# compute reflection
|
||||
reflected_ray_dir = (ray.dir - N * 2. * ray.dir.dot(N)).normalize()
|
||||
|
||||
nudged = hit.point + N * .000001 # M nudged to avoid itself
|
||||
color += (scene.ambient_color + get_raycolor(Ray(nudged, reflected_ray_dir, ray.depth + 1, ray.n, ray.reflections + 1, ray.transmissions, ray.diffuse_reflections), scene))*F
|
||||
|
||||
|
||||
|
||||
# because the film is very thin (nm) we ignore refraction for transmitted ray.
|
||||
|
||||
transmitted_ray_dir = ray.dir
|
||||
nudged = hit.point - N * .000001 #nudged for transmitted ray
|
||||
T = 1. - F
|
||||
transmitted_color = get_raycolor( Ray(nudged, transmitted_ray_dir, ray.depth + 1, ray.n, ray.reflections, ray.transmissions + 1, ray.diffuse_reflections), scene )*T
|
||||
color += transmitted_color
|
||||
return color
|
||||
BIN
Programming/TRAK/sightpy/normalmaps/floor.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
113
Programming/TRAK/sightpy/ray.py
Normal file
@ -0,0 +1,113 @@
|
||||
from .utils.constants import *
|
||||
from .utils.vector3 import vec3, extract, rgb
|
||||
import numpy as np
|
||||
from functools import reduce as reduce
|
||||
|
||||
RAYS = 0
|
||||
PRIMARY_RAYS = 0
|
||||
|
||||
|
||||
class Ray:
|
||||
"""Info of the ray and the media it's travelling"""
|
||||
def __init__(self,origin, dir, depth, n, reflections, transmissions, diffuse_reflections):
|
||||
|
||||
self.origin = origin # the point where the ray comes from
|
||||
self.dir = dir # direction of the ray
|
||||
self.depth = depth # ray_depth is the number of the refrections + transmissions/refractions, starting at zero for camera rays
|
||||
self.n = n # ray_n is the index of refraction of the media in which the ray is travelling
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
self.reflections = reflections #reflections is the number of the refrections, starting at zero for camera rays
|
||||
self.transmissions = transmissions #transmissions is the number of the transmissions/refractions, starting at zero for camera rays
|
||||
self.diffuse_reflections = diffuse_reflections #reflections is the number of the refrections, starting at zero for camera rays
|
||||
|
||||
global RAYS
|
||||
RAYS += 1
|
||||
if self.reflections == 0 and self.transmissions == 0 and self.diffuse_reflections == 0:
|
||||
global PRIMARY_RAYS
|
||||
PRIMARY_RAYS += 1
|
||||
|
||||
|
||||
def extract(self,hit_check):
|
||||
return Ray(self.origin.extract(hit_check), self.dir.extract(hit_check), self.depth, self.n.extract(hit_check), self.reflections, self.transmissions,self.diffuse_reflections)
|
||||
|
||||
|
||||
class Hit:
|
||||
"""Info of the ray-surface intersection"""
|
||||
def __init__(self, distance, orientation, material, collider,surface):
|
||||
self.distance = distance
|
||||
self.orientation = orientation
|
||||
self.material = material
|
||||
self.collider = collider
|
||||
self.surface = surface
|
||||
self.u = None
|
||||
self.v = None
|
||||
self.N = None
|
||||
self.point = None
|
||||
|
||||
def get_uv(self):
|
||||
if self.u is None: #this is for prevent multiple computations of u,v
|
||||
self.u, self.v = self.collider.assigned_primitive.get_uv(self)
|
||||
return self.u, self.v
|
||||
|
||||
def get_normal(self):
|
||||
if self.N is None: #this is for prevent multiple computations of normal
|
||||
self.N = self.collider.get_N(self)
|
||||
return self.N
|
||||
|
||||
|
||||
|
||||
def get_raycolor(ray, scene):
|
||||
|
||||
inters = [s.intersect(ray.origin, ray.dir) for s in scene.collider_list]
|
||||
distances, hit_orientation = zip(*inters)
|
||||
|
||||
|
||||
# get the shortest distance collision
|
||||
nearest = reduce(np.minimum, distances)
|
||||
color = rgb(0., 0., 0.)
|
||||
|
||||
|
||||
for (coll, dis , orient) in zip(scene.collider_list, distances, hit_orientation):
|
||||
hit_check = (nearest != FARAWAY) & (dis == nearest)
|
||||
|
||||
if np.any(hit_check):
|
||||
|
||||
material = coll.assigned_primitive.material
|
||||
hit_info = Hit(extract(hit_check,dis) , extract(hit_check,orient), material, coll, coll.assigned_primitive)
|
||||
|
||||
|
||||
|
||||
cc = material.get_color(scene, ray.extract(hit_check), hit_info)
|
||||
color += cc.place(hit_check)
|
||||
|
||||
return color
|
||||
|
||||
|
||||
|
||||
|
||||
def get_distances(ray, scene): #Used for debugging ray-surface collisions. Return a grey map of objects distances.
|
||||
|
||||
inters = [s.intersect(ray.origin, ray.dir) for s in scene.collider_list]
|
||||
distances, hit_orientation = zip(*inters)
|
||||
# get the shortest distance collision
|
||||
nearest = reduce(np.minimum, distances)
|
||||
|
||||
max_r_distance = 10
|
||||
r_distance = np.where(nearest <= max_r_distance, nearest, max_r_distance)
|
||||
norm_r_distance = r_distance/max_r_distance
|
||||
return rgb(norm_r_distance, norm_r_distance, norm_r_distance)
|
||||
|
||||
def get_rays():
|
||||
return RAYS, PRIMARY_RAYS
|
||||
|
||||
|
||||
|
||||
119
Programming/TRAK/sightpy/scene.py
Normal file
@ -0,0 +1,119 @@
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import time
|
||||
from .utils import colour_functions as cf
|
||||
from .camera import Camera
|
||||
from .utils.vector3 import vec3, rgb
|
||||
from .ray import get_raycolor, get_distances, get_rays
|
||||
from . import lights
|
||||
from .backgrounds.skybox import SkyBox
|
||||
from .backgrounds.panorama import Panorama
|
||||
|
||||
|
||||
class Scene():
|
||||
def __init__(self, ambient_color = rgb(0.01, 0.01, 0.01), n = vec3(1.0,1.0,1.0)) :
|
||||
# n = index of refraction (by default index of refraction of air n = 1.)
|
||||
|
||||
self.scene_primitives = []
|
||||
self.collider_list = []
|
||||
self.shadowed_collider_list = []
|
||||
self.Light_list = []
|
||||
self.importance_sampled_list = []
|
||||
self.ambient_color = ambient_color
|
||||
self.n = n
|
||||
self.importance_sampled_list = []
|
||||
def add_Camera(self, look_from, look_at, **kwargs):
|
||||
self.camera = Camera(look_from, look_at, **kwargs)
|
||||
|
||||
|
||||
def add_PointLight(self, pos, color):
|
||||
self.Light_list += [lights.PointLight(pos, color)]
|
||||
|
||||
def add_DirectionalLight(self, Ldir, color):
|
||||
self.Light_list += [lights.DirectionalLight(Ldir.normalize(), color)]
|
||||
|
||||
def add(self,primitive, importance_sampled = False):
|
||||
self.scene_primitives += [primitive]
|
||||
self.collider_list += primitive.collider_list
|
||||
|
||||
if importance_sampled == True:
|
||||
self.importance_sampled_list += [primitive]
|
||||
|
||||
if primitive.shadow == True:
|
||||
self.shadowed_collider_list += primitive.collider_list
|
||||
|
||||
|
||||
def add_Background(self, img, light_intensity = 0.0, blur =0.0 , spherical = False):
|
||||
|
||||
primitive = None
|
||||
if spherical == False:
|
||||
primitive = SkyBox(img, light_intensity = light_intensity, blur = blur)
|
||||
else:
|
||||
primitive = Panorama(img, light_intensity = light_intensity, blur = blur)
|
||||
|
||||
self.scene_primitives += [primitive]
|
||||
self.collider_list += primitive.collider_list
|
||||
|
||||
|
||||
def render(self, samples_per_pixel, progress_bar = False):
|
||||
|
||||
print ("Rendering...")
|
||||
|
||||
t0 = time.time()
|
||||
color_RGBlinear = rgb(0.,0.,0.)
|
||||
|
||||
if progress_bar == True:
|
||||
|
||||
|
||||
try:
|
||||
import progressbar
|
||||
|
||||
except ModuleNotFoundError:
|
||||
print("progressbar module is required. \nRun: pip install progressbar")
|
||||
|
||||
bar = progressbar.ProgressBar()
|
||||
for i in bar(range(samples_per_pixel)):
|
||||
color_RGBlinear += get_raycolor(self.camera.get_ray(self.n), scene = self)
|
||||
bar.update(i)
|
||||
else:
|
||||
|
||||
|
||||
for i in range(samples_per_pixel):
|
||||
color_RGBlinear += get_raycolor(self.camera.get_ray(self.n), scene = self)
|
||||
|
||||
|
||||
|
||||
#average samples per pixel (antialiasing)
|
||||
color_RGBlinear = color_RGBlinear/samples_per_pixel
|
||||
#gamma correction
|
||||
color = cf.sRGB_linear_to_sRGB(color_RGBlinear.to_array())
|
||||
|
||||
RAYS, PRIMARY_RAYS = get_rays()
|
||||
|
||||
print (f"Render Took: {round(time.time() - t0, 2)}s\n"
|
||||
f"resolution: {self.camera.screen_width}x{self.camera.screen_height}\n"
|
||||
f"rays (total): {RAYS}\n"
|
||||
f"rays (primary): {PRIMARY_RAYS}\n"
|
||||
f"rays (secondary): {RAYS-PRIMARY_RAYS}")
|
||||
|
||||
img_RGB = []
|
||||
for c in color:
|
||||
# average ray colors that fall in the same pixel. (antialiasing)
|
||||
img_RGB += [Image.fromarray((255 * np.clip(c, 0, 1).reshape((self.camera.screen_height, self.camera.screen_width))).astype(np.uint8), "L") ]
|
||||
|
||||
return Image.merge("RGB", img_RGB)
|
||||
|
||||
|
||||
def get_distances(self): #Used for debugging ray-primitive collisions. Return a grey map of objects distances.
|
||||
|
||||
print ("Rendering...")
|
||||
t0 = time.time()
|
||||
color_RGBlinear = get_distances( self.camera.get_ray(self.n), scene = self)
|
||||
#gamma correction
|
||||
color = color_RGBlinear.to_array()
|
||||
|
||||
print ("Render Took", time.time() - t0)
|
||||
|
||||
|
||||
img_RGB = [Image.fromarray((255 * np.clip(c, 0, 1).reshape((self.camera.screen_height, self.camera.screen_width))).astype(np.uint8), "L") for c in color]
|
||||
return Image.merge("RGB", img_RGB)
|
||||
1
Programming/TRAK/sightpy/textures/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .texture import texture, image, solid_color
|
||||
BIN
Programming/TRAK/sightpy/textures/checkered_floor.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
Programming/TRAK/sightpy/textures/noise.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
38
Programming/TRAK/sightpy/textures/texture.py
Normal file
@ -0,0 +1,38 @@
|
||||
from ..utils.constants import *
|
||||
from ..utils.vector3 import vec3, rgb
|
||||
from ..ray import Ray, get_raycolor
|
||||
from ..utils.image_functions import load_image, load_image_as_linear_sRGB
|
||||
import numpy as np
|
||||
from abc import abstractmethod
|
||||
|
||||
class texture():
|
||||
|
||||
@abstractmethod
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_color(self, hit):
|
||||
pass
|
||||
|
||||
|
||||
class solid_color(texture):
|
||||
|
||||
def __init__(self,color):
|
||||
self.color = color
|
||||
|
||||
def get_color(self, hit):
|
||||
return self.color
|
||||
|
||||
|
||||
class image(texture):
|
||||
|
||||
def __init__(self,img, repeat = 1.0):
|
||||
self.img = load_image_as_linear_sRGB("sightpy/textures/" + img)
|
||||
self.repeat = repeat
|
||||
|
||||
def get_color(self, hit):
|
||||
u,v = hit.get_uv()
|
||||
im = self.img[-((v * self.img.shape[0]*self.repeat ).astype(int)% self.img.shape[0]) , (u * self.img.shape[1]*self.repeat).astype(int) % self.img.shape[1] ].T
|
||||
color = vec3(im[0],im[1],im[2])
|
||||
return color
|
||||
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 86 KiB |
BIN
Programming/TRAK/sightpy/textures/wood.jpg
Normal file
|
After Width: | Height: | Size: 91 KiB |
0
Programming/TRAK/sightpy/utils/__init__.py
Normal file
22
Programming/TRAK/sightpy/utils/colour_functions.py
Normal file
@ -0,0 +1,22 @@
|
||||
import numpy as np
|
||||
|
||||
def sRGB_linear_to_sRGB(rgb_linear):
|
||||
|
||||
'''sRGB standard for gamma inverse correction.'''
|
||||
rgb = np.where( rgb_linear <= 0.00304, 12.92 * rgb_linear, 1.055 * np.power(rgb_linear, 1.0/2.4) - 0.055)
|
||||
|
||||
# clip intensity if needed (rgb values > 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
|
||||
|
||||
5
Programming/TRAK/sightpy/utils/constants.py
Normal file
@ -0,0 +1,5 @@
|
||||
UPWARDS = 1
|
||||
UPDOWN = -1
|
||||
FARAWAY = 1.0e39
|
||||
SKYBOX_DISTANCE = 1.0e6
|
||||
|
||||
40
Programming/TRAK/sightpy/utils/image_functions.py
Normal file
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
231
Programming/TRAK/sightpy/utils/random.py
Normal file
@ -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
|
||||
188
Programming/TRAK/sightpy/utils/vector3.py
Normal file
@ -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
|
||||
13
Programming/TRAK/utils.py
Normal file
@ -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.")
|
||||