aboutsummaryrefslogtreecommitdiff
path: root/firmament
diff options
context:
space:
mode:
Diffstat (limited to 'firmament')
-rw-r--r--firmament/__init__.py3
-rw-r--r--firmament/planets.py11
-rw-r--r--firmament/raymarch.cl209
-rw-r--r--firmament/renderer.py51
-rw-r--r--firmament/sun.py32
5 files changed, 306 insertions, 0 deletions
diff --git a/firmament/__init__.py b/firmament/__init__.py
new file mode 100644
index 0000000..26d56ec
--- /dev/null
+++ b/firmament/__init__.py
@@ -0,0 +1,3 @@
+from . import renderer
+
+Renderer = renderer.Renderer
diff --git a/firmament/planets.py b/firmament/planets.py
new file mode 100644
index 0000000..4570019
--- /dev/null
+++ b/firmament/planets.py
@@ -0,0 +1,11 @@
+earth = {
+ 'earth_radius': 6371e3,
+ 'atmos_height': 100e3,
+
+ 'rayleigh_atmos_height': 7994,
+ 'mie_atmos_height' : 1200,
+
+ 'rayleigh_beta': (5.4717e-6, 12.7853e-6, 31.2142e-6),
+ 'mie_beta' : (21e-6),
+ 'mie_g' : 0.75
+}
diff --git a/firmament/raymarch.cl b/firmament/raymarch.cl
new file mode 100644
index 0000000..d0092d9
--- /dev/null
+++ b/firmament/raymarch.cl
@@ -0,0 +1,209 @@
+__constant double earth_radius = $earth_radius;
+__constant double atmos_height = $atmos_height;
+
+__constant double3 rayleigh_beta = (double3)$rayleigh_beta;
+__constant double3 mie_beta = (double3)$mie_beta;
+__constant double mie_g = $mie_g;
+
+__constant int ray_samples = $ray_samples;
+__constant int light_samples = $light_samples;
+
+__constant double exposure = $exposure;
+
+bool insideAtmosphere(double3 pos) {
+ return length(pos) < earth_radius + atmos_height;
+}
+
+double altitude(double3 pos) {
+ return length(pos) - earth_radius;
+}
+
+/// Solve x^2 + px + q
+bool solvePolynomialOfDegreeTwo(double p, double q, double* x1, double* x2) {
+ const double pHalf = 0.5*p;
+ const double inner = pHalf*pHalf - q;
+
+ if (inner >= 0.0) {
+ *x1 = -pHalf + sqrt(inner);
+ *x2 = -pHalf - sqrt(inner);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/// Interection of origin + d*dir and a sphere of radius r for normalized dir
+bool solveRaySphereIntersection(double3 origin, double3 dir, double r, double* d0, double* d1) {
+ const double p = 2 * dot(dir,origin);
+ const double q = dot(origin,origin) - r*r;
+
+ if (solvePolynomialOfDegreeTwo(p, q, d0, d1)) {
+ if (*d0 > *d1) {
+ double tmp = *d1;
+ *d1 = *d0;
+ *d0 = tmp;
+ }
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/// Map {0,...,screenX}x{0,...,screenY} to [-1,1]^2
+double2 getNormalizedScreenPos(double x, double y) {
+ return (double2)(
+ 2.0 * (0.5 - x / $size_x) * $size_x.0 / $size_y.0,
+ 2.0 * (0.5 - y / $size_y)
+ );
+}
+
+/// Pinhole camera
+double3 getEyeRayDir(double2 screen_pos, double3 eye_pos, double3 eye_target) {
+ const double3 forward = normalize(eye_target - eye_pos);
+ const double3 right = normalize(cross((double3)(0.0, 0.0, -1.0), forward));
+ const double3 up = normalize(cross(forward, right));
+
+ return normalize(screen_pos.x*right + screen_pos.y*up + $zoom*forward);
+}
+
+/// Fisheye camera
+bool inFishEyeView(double2 screen_pos) {
+ return length(screen_pos) <= 1.0;
+}
+
+double3 getFishEyeRayDir(double2 screen_pos) {
+ const double phi = atan2(screen_pos.y, screen_pos.x);
+ const double theta = acos(1.0 - (screen_pos.x*screen_pos.x + screen_pos.y*screen_pos.y));
+ return (double3)(sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta));
+}
+
+/// Return true iff earth is hit by rays along dir
+bool isVisible(double3 origin, double3 dir) {
+ double e0, e1;
+ return !solveRaySphereIntersection(origin, dir, earth_radius, &e0, &e1)
+ || (e0 < 0 && e1 < 0);
+}
+
+/// Return distance between entering and exiting the atmosphere
+double lengthOfRayInAtmosphere(double3 origin, double3 dir) {
+ double d0, d1;
+ solveRaySphereIntersection(origin, dir, earth_radius + atmos_height, &d0, &d1);
+ return d1 - d0;
+}
+
+/// Return light depths of secondary rays
+double2 lightDepth(double3 curr, double3 sun) {
+ const double h = lengthOfRayInAtmosphere(curr, sun) / light_samples;
+
+ double2 depth = 0.0;
+
+ for (unsigned i = 0; i < light_samples; ++i) {
+ const double height = altitude(curr + (i+0.5)*h*sun);
+ depth += exp(-height / (double2)($rayleigh_atmos_height, $mie_atmos_height)) * h;
+ }
+
+ return depth;
+}
+
+/// Calculate color of light along ray
+double3 scatter(double3 origin, double3 dir, double dist, double3 sun) {
+ double3 rayleigh_sum = 0.0;
+ double3 mie_sum = 0.0;
+
+ double rayleigh_depth = 0.0;
+ double mie_depth = 0.0;
+
+ const double h = dist / ray_samples;
+
+ for (unsigned i = 0; i < ray_samples; ++i) {
+ double3 curr = origin + (i+0.5)*h*dir;
+
+ if (isVisible(curr, sun)) {
+ const double height = altitude(curr);
+
+ const double rayleigh_h = exp(-height / $rayleigh_atmos_height) * h;
+ const double mie_h = exp(-height / $mie_atmos_height) * h;
+
+ rayleigh_depth += rayleigh_h;
+ mie_depth += mie_h;
+
+ const double2 light_depth = lightDepth(curr, sun);
+ const double3 tau = rayleigh_beta * (rayleigh_depth + light_depth.x)
+ + mie_beta * (mie_depth + light_depth.y);
+ const double3 attenuation = exp(-tau);
+
+ rayleigh_sum += attenuation * rayleigh_h;
+ mie_sum += attenuation * mie_h;
+ }
+ }
+
+ const double mu = dot(dir,sun);
+
+ const double rayleigh_phase = 0.75 * (1.0 + mu*mu);
+ const double mie_phase = 0.5*(1-mie_g*mie_g)/pow(1+mie_g*mie_g-2*mie_g*mu,1.5);
+
+ return rayleigh_sum*rayleigh_beta*rayleigh_phase + mie_sum*mie_beta*mie_phase;
+}
+
+void setColor(__global double* result, unsigned x, unsigned y, double3 color) {
+ result[3*$size_x*y + 3*x + 0] = color.x;
+ result[3*$size_x*y + 3*x + 1] = color.y;
+ result[3*$size_x*y + 3*x + 2] = color.z;
+}
+
+void render(__global double* result, unsigned x, unsigned y, double3 origin, double3 ray_dir, double3 sun) {
+ double d0, d1;
+ if (!solveRaySphereIntersection(origin, ray_dir, earth_radius + atmos_height, &d0, &d1)) {
+ setColor(result, x, y, 0.0);
+ return;
+ }
+
+ double min_dist = d0;
+ double max_dist = d1;
+
+ if (insideAtmosphere(origin)) {
+ min_dist = 0.0;
+
+ if (solveRaySphereIntersection(origin, ray_dir, earth_radius, &d0, &d1) && d1 > 0) {
+ max_dist = max(0.0, d0);
+ }
+ } else {
+ if (solveRaySphereIntersection(origin, ray_dir, earth_radius, &d0, &d1)) {
+ max_dist = d0;
+ }
+ }
+
+ const double3 ray_origin = origin + min_dist*ray_dir;
+ const double ray_length = max_dist - min_dist;
+
+ double3 color = scatter(ray_origin, ray_dir, ray_length, normalize(sun));
+ color = 1.0 - exp(-exposure * color);
+
+ setColor(result, x, y, color);
+}
+
+__kernel void render_fisheye(__global double* result, double3 eye_pos, double3 eye_dir, double3 sun) {
+ const unsigned x = get_global_id(0);
+ const unsigned y = get_global_id(1);
+
+ const double2 screen_pos = getNormalizedScreenPos(x, y);
+
+ if (!inFishEyeView(screen_pos)) {
+ setColor(result, x, y, 1.0);
+ return;
+ }
+
+ const double3 ray_dir = getFishEyeRayDir(screen_pos);
+
+ render(result, x, y, eye_pos, ray_dir, sun);
+}
+
+__kernel void render_pinhole(__global double* result, double3 eye_pos, double3 eye_dir, double3 sun) {
+ const unsigned x = get_global_id(0);
+ const unsigned y = get_global_id(1);
+
+ const double2 screen_pos = getNormalizedScreenPos(x, y);
+ const double3 ray_dir = getEyeRayDir(screen_pos, eye_pos, eye_pos + eye_dir);
+
+ render(result, x, y, eye_pos, ray_dir, sun);
+}
diff --git a/firmament/renderer.py b/firmament/renderer.py
new file mode 100644
index 0000000..d020cdb
--- /dev/null
+++ b/firmament/renderer.py
@@ -0,0 +1,51 @@
+import numpy
+from string import Template
+
+import pyopencl as cl
+from pyopencl.cltypes import make_double3
+
+mf = cl.mem_flags
+
+class Renderer:
+ def __init__(self, config, planet):
+ self.config = config
+ self.planet = planet
+
+ self.cl_platform = cl.get_platforms()[0]
+ self.cl_context = cl.Context(properties=[(cl.context_properties.PLATFORM, self.cl_platform)])
+ self.cl_queue = cl.CommandQueue(self.cl_context)
+
+ self.cl_picture = cl.Buffer(self.cl_context, mf.WRITE_ONLY, size=self.config['size_x']*self.config['size_y']*3*numpy.float64(0).nbytes)
+
+ with open('firmament/raymarch.cl') as f:
+ self.program = cl.Program(self.cl_context, Template(f.read()).substitute(
+ {**self.config, **self.planet}
+ )).build()
+
+ def render_pinhole(self, sun):
+ self.program.render_pinhole(
+ self.cl_queue,
+ (self.config['size_x'], self.config['size_y']),
+ None,
+ self.cl_picture,
+ make_double3(*(self.config['eye_pos'] * self.planet['earth_radius'])),
+ make_double3(*(self.config['eye_dir'] * self.planet['earth_radius'])),
+ make_double3(*sun))
+
+ np_picture = numpy.ndarray(shape=(self.config['size_y'], self.config['size_x'], 3), dtype=numpy.float64)
+ cl.enqueue_copy(self.cl_queue, np_picture, self.cl_picture).wait();
+ return np_picture
+
+ def render_fisheye(self, sun):
+ self.program.render_fisheye(
+ self.cl_queue,
+ (self.config['size_x'], self.config['size_y']),
+ None,
+ self.cl_picture,
+ make_double3(*(self.config['eye_pos'] * self.planet['earth_radius'])),
+ make_double3(*(self.config['eye_dir'] * self.planet['earth_radius'])),
+ make_double3(*sun))
+
+ np_picture = numpy.ndarray(shape=(self.config['size_y'], self.config['size_x'], 3), dtype=numpy.float64)
+ cl.enqueue_copy(self.cl_queue, np_picture, self.cl_picture).wait();
+ return np_picture
diff --git a/firmament/sun.py b/firmament/sun.py
new file mode 100644
index 0000000..7ceec5d
--- /dev/null
+++ b/firmament/sun.py
@@ -0,0 +1,32 @@
+import numpy as np
+from datetime import datetime
+
+## Sun direction depending on time and place
+# As described in Appendix D of "ME 4131 Thermal Environmental Engineering Laboratory Manual"
+
+def sun_declination(time):
+ day_of_year = time.timetuple().tm_yday
+ return 23.45 * np.sin(np.radians((360/365)*(284+day_of_year)))
+
+def equation_of_time(time):
+ day_of_year = time.timetuple().tm_yday
+ b = np.radians(360*(day_of_year-81)/364)
+ return 0.165*np.sin(2*b) - 0.126*np.cos(b) - 0.025*np.sin(b)
+
+def sun_direction(lat, lon, time, time_diff, summertime_shift = 0):
+ lon_std = time_diff * 15
+ clock_time = time.hour + time.minute/60
+ local_solar_time = clock_time + (1/15)*(lon - lon_std) + equation_of_time(time) - summertime_shift
+ hour_angle = 15*(local_solar_time - 12)
+
+ l = np.radians(lat)
+ h = np.radians(hour_angle)
+ d = np.radians(sun_declination(time))
+
+ altitude = np.arcsin(np.cos(l) * np.cos(h) * np.cos(d) + np.sin(l) * np.sin(d))
+ azimuth = np.arccos((np.cos(d) * np.sin(l) * np.cos(h) - np.sin(d) * np.cos(l)) / np.cos(altitude))
+
+ if h < 0:
+ return (altitude, azimuth)
+ else:
+ return (altitude, -azimuth)