aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Kummerlaender2019-12-19 13:07:02 +0100
committerAdrian Kummerlaender2019-12-19 13:18:41 +0100
commit923ad5e83be561dcadfec113177cd7cbcaea56d9 (patch)
treef6d254fdf7da328ca4318cfd089921759d60792e
downloadfirmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar.gz
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar.bz2
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar.lz
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar.xz
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.tar.zst
firmament-923ad5e83be561dcadfec113177cd7cbcaea56d9.zip
Basic sky rendering using the model by Nishita et al.
See "Display of the earth taking into account atmospheric scattering" (Nishita et al., 1993) `bluedot.py` renders the full earth disk while rotating the sun around it. `sunrise.py` renders the rise of the sun. Physical model constants may be changed in `planets.py`.
-rw-r--r--bluedot.py50
-rw-r--r--planets.py10
-rw-r--r--raymarch.cl175
-rw-r--r--shell.nix26
-rw-r--r--sunrise.py50
5 files changed, 311 insertions, 0 deletions
diff --git a/bluedot.py b/bluedot.py
new file mode 100644
index 0000000..036ae7f
--- /dev/null
+++ b/bluedot.py
@@ -0,0 +1,50 @@
+import numpy
+import matplotlib.pyplot as plt
+from string import Template
+
+import pyopencl as cl
+from pyopencl.cltypes import make_double3
+
+mf = cl.mem_flags
+
+from planets import earth
+
+config = {
+ 'size_x': 1000,
+ 'size_y': 1000,
+
+ 'ray_samples': 16,
+ 'light_samples': 8,
+
+ 'exposure': 20.0,
+
+ 'eye_pos': (0, -3, 1.1),
+ 'eye_dir': (0, 1, -0.35)
+}
+
+cl_platform = cl.get_platforms()[0]
+cl_context = cl.Context(properties=[(cl.context_properties.PLATFORM, cl_platform)])
+cl_queue = cl.CommandQueue(cl_context)
+
+cl_picture = cl.Buffer(cl_context, mf.WRITE_ONLY, size=config['size_x']*config['size_y']*3*numpy.float64(0).nbytes)
+program = None
+
+with open('raymarch.cl') as f:
+ program = cl.Program(cl_context, Template(f.read()).substitute(
+ {**config, **earth}
+ )).build()
+
+for i in range(0,360):
+ sun = make_double3(numpy.cos(i*2*numpy.pi/360),numpy.sin(i*2*numpy.pi/360),0)
+ print(sun)
+
+ program.render(
+ cl_queue, (config['size_x'], config['size_y']), None, cl_picture,
+ make_double3(*config['eye_pos']),
+ make_double3(*config['eye_dir']),
+ sun)
+
+ np_picture = numpy.ndarray(shape=(config['size_y'], config['size_x'], 3), dtype=numpy.float64)
+ cl.enqueue_copy(cl_queue, np_picture, cl_picture).wait();
+
+ plt.imsave('sky_%03d.png' % i, np_picture, origin='lower')
diff --git a/planets.py b/planets.py
new file mode 100644
index 0000000..f94782c
--- /dev/null
+++ b/planets.py
@@ -0,0 +1,10 @@
+earth = {
+ 'earth_radius': 6371e3,
+ 'atmos_height': 100e3,
+
+ 'rayleigh_atmos_height': 7994,
+ 'mie_atmos_height' : 1200,
+
+ 'rayleigh_beta': (3.8e-6, 13.5e-6, 33.1e-6),
+ 'mie_beta' : (21e-6)
+}
diff --git a/raymarch.cl b/raymarch.cl
new file mode 100644
index 0000000..d6a771d
--- /dev/null
+++ b/raymarch.cl
@@ -0,0 +1,175 @@
+__constant double earth_radius = $earth_radius;
+__constant double atmos_height = $atmos_height;
+
+__constant double rayleigh_atmos_height = $rayleigh_atmos_height;
+__constant double mie_atmos_height = $mie_atmos_height;
+
+__constant double3 rayleigh_beta = (double3)$rayleigh_beta;
+__constant double3 mie_beta = (double3)$mie_beta;
+
+__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;
+ }
+}
+
+double2 getNormalizedScreenPos(double x, double y) {
+ return (double2)(
+ 2.0 * (x / $size_x - 0.5) * $size_x.0 / $size_y.0,
+ 2.0 * (y / $size_y - 0.5)
+ );
+}
+
+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 + 1.5*forward);
+}
+
+bool isVisible(double3 origin, double3 dir) {
+ double e0, e1;
+ return !solveRaySphereIntersection(origin, dir, earth_radius, &e0, &e1)
+ || (e0 < 0 && e1 < 0);
+}
+
+double lengthOfRayInAtmosphere(double3 origin, double3 dir) {
+ double d0, d1;
+ solveRaySphereIntersection(origin, dir, earth_radius + atmos_height, &d0, &d1);
+ return d1 - d0;
+}
+
+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 (int i = 0; i < ray_samples; ++i) {
+ double3 curr = origin + (i+0.5)*h*dir;
+
+ if (isVisible(curr, sun)) {
+ double height = altitude(curr);
+
+ double rayleigh_h = exp(-height / rayleigh_atmos_height) * h;
+ double mie_h = exp(-height / mie_atmos_height) * h;
+
+ rayleigh_depth += rayleigh_h;
+ mie_depth += mie_h;
+
+ double light_h = lengthOfRayInAtmosphere(curr, sun) / light_samples;
+
+ double rayleigh_light_depth = 0;
+ double mie_light_depth = 0;
+
+ for (int j = 0; j < light_samples; ++j) {
+ height = altitude(curr + (j+0.5)*light_h*sun);
+
+ rayleigh_light_depth += exp(-height / rayleigh_atmos_height) * light_h;
+ mie_light_depth += exp(-height / mie_atmos_height) * light_h;
+ }
+
+ double3 tau = rayleigh_beta*(rayleigh_depth+rayleigh_light_depth) + mie_beta*(mie_depth+ mie_light_depth);
+ double3 attenuation = exp(-tau);
+
+ rayleigh_sum += attenuation * rayleigh_h;
+ mie_sum += attenuation * mie_h;
+ }
+ }
+
+ const double mu = dot(dir,sun);
+ const double rayleigh_phase = 3. / (16.*M_PI) * (1.0 + mu*mu);
+ const double g = 0.8;
+ const double mie_phase = 3. / (8.*M_PI) * ((1. - g*g) * (1. + mu*mu)) / ((2. + g*g) * pow(1. + g*g - 2.*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;
+}
+
+__kernel void render(__global double* result, double3 eye_pos, double3 eye_dir, double3 sun) {
+ eye_pos *= earth_radius;
+ eye_dir *= earth_radius;
+
+ const unsigned x = get_global_id(0);
+ const unsigned y = get_global_id(1);
+
+ double2 screen_pos = getNormalizedScreenPos(x, y);
+ double3 ray_dir = getEyeRayDir(screen_pos, eye_pos, eye_pos + eye_dir);
+
+ double d0, d1;
+
+ if (!solveRaySphereIntersection(eye_pos, 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(eye_pos)) {
+ min_dist = 0.0;
+
+ if (solveRaySphereIntersection(eye_pos, ray_dir, earth_radius, &d0, &d1) && d1 > 0) {
+ max_dist = max(0.0, d0);
+ }
+ } else {
+ if (solveRaySphereIntersection(eye_pos, ray_dir, earth_radius, &d0, &d1)) {
+ max_dist = d0;
+ }
+ }
+
+ const double3 ray_origin = eye_pos + 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);
+}
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..5f57bc8
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,26 @@
+{ pkgs ? import <nixpkgs> { }, ... }:
+
+pkgs.stdenvNoCC.mkDerivation rec {
+ name = "pycl-env";
+ env = pkgs.buildEnv { name = name; paths = buildInputs; };
+
+ buildInputs = let
+ local-python = pkgs.python3.withPackages (python-packages: with python-packages; [
+ numpy
+ sympy
+ pyopencl setuptools
+ matplotlib
+ ]);
+
+ in [
+ local-python
+ pkgs.opencl-info
+ pkgs.universal-ctags
+ ];
+
+ shellHook = ''
+ export NIX_SHELL_NAME="${name}"
+ export PYOPENCL_COMPILER_OUTPUT=1
+ export PYTHONPATH="$PWD:$PYTHONPATH"
+ '';
+}
diff --git a/sunrise.py b/sunrise.py
new file mode 100644
index 0000000..b7b2cd3
--- /dev/null
+++ b/sunrise.py
@@ -0,0 +1,50 @@
+import numpy
+import matplotlib.pyplot as plt
+from string import Template
+
+import pyopencl as cl
+from pyopencl.cltypes import make_double3
+
+mf = cl.mem_flags
+
+from planets import earth
+
+config = {
+ 'size_x': 480,
+ 'size_y': 270,
+
+ 'ray_samples' : 64,
+ 'light_samples': 32,
+
+ 'exposure': 20.0,
+
+ 'eye_pos': (0, 0, 1.00001),
+ 'eye_dir': (0, 1, 0.3)
+}
+
+cl_platform = cl.get_platforms()[0]
+cl_context = cl.Context(properties=[(cl.context_properties.PLATFORM, cl_platform)])
+cl_queue = cl.CommandQueue(cl_context)
+
+cl_picture = cl.Buffer(cl_context, mf.WRITE_ONLY, size=config['size_x']*config['size_y']*3*numpy.float64(0).nbytes)
+program = None
+
+with open('raymarch.cl') as f:
+ program = cl.Program(cl_context, Template(f.read()).substitute(
+ {**config, **earth}
+ )).build()
+
+for i in range(-10,10):
+ sun = make_double3(0.0,numpy.cos(i*2*numpy.pi/360),numpy.sin(i*2*numpy.pi/360))
+ print(sun)
+
+ program.render(
+ cl_queue, (config['size_x'], config['size_y']), None, cl_picture,
+ make_double3(*config['eye_pos']),
+ make_double3(*config['eye_dir']),
+ sun)
+
+ np_picture = numpy.ndarray(shape=(config['size_y'], config['size_x'], 3), dtype=numpy.float64)
+ cl.enqueue_copy(cl_queue, np_picture, cl_picture).wait();
+
+ plt.imsave('sky_%03d.png' % (i+10), np_picture, origin='lower')