sandbox/Antoonvh/bwatch.h
Bwatch
Bwatch
is a ray-casting toolbox written in Basilisk C.
It should work with Octrees or multigrid3D.
“Installation”
Apart from the default Basilisk code, the implementation is scattered among a few header files.
Required files are:
Bwatch.h
(this file) contains a bunch of generic definitionsSketch.h
implements high-level rendering functions and most of the user interfaceBwatch-iterators.h
defines low-level functions and iterators to help find ray-object intersections efficiently.Radix.h
provides a sorting algorithm that maybe used for volumetric rendering.
Copy and place them in your current folder, a
$BASILISK_INCLUDE_PATH
or against my advice in
$BASILISK
.
Optional is:
- installing an adaptive raycaster or (this one) if you want to use
store_adaptive()
- Having
convert
installed on your system forimage
andtext
rendering. - Watching along the rendering process
How does it work?
Rays are casted from the camera into the scene. The nearest intersection is computed with the obects that the user called. The standard lightning/shading model for coloured materials follows that of Bui Tuong Phong.
tips
- Compile your bwatch code with the
-fopenmp
(gcc) or-qopenmp
(icc) compiler flag (before the*.c
file) to accelerate rendering.
- When your rendering invokes secondary rays (e.g. transmission,
reflection etc.), make sure the scene has a background. For example, a
large
dull
sphere. - Compile with
-disable-dimensions
to avoid qcc crashing inget_dimension()
routine.
Implementation
#include "fractions.h"
A list of sketch functions is stored. Rendering takes place when an
image is store()
d.
#define N_SKETCH 256 // Maximum sketch functions
typedef double (*sketch)(); // a Sketch function prototype
[N_SKETCH]; // List sketching functions
sketch sketch_listint functot; // number of sketching function calls
scalar * shading = NULL; // VOF objects that cast shade
scalar smoke; // Concentration field that cast shade
//Prim * Obj = NULL; // Primitives should cast a shade as well
int max_ray_depth = 5; // Number of ray recasts
attribute { // attenuation coefficient of smoke fields
double att;
}
// A camera class and "the" cam
struct Camera {
; // camera pos
coord O; // upward pointing vector (related to image vertical)
coord up; // In case up-vector is parallel to the viewing direction
coord rhs; // Pointing towards
coord poidouble fov; // horizontal width fustrum through poi.
int nx, ny; // Resolution
double min, max; // Sensitivities
double (*f)(); // Transfer function
};
struct Camera cam = {.O = {0., 0., 3}, // view from back to front (-z)
.up = {0., 1., 0.},
.rhs = {1., 0., 0.},
.poi = {0., 0., 0.}, // view from back to front (-z)
.fov = 1.1,
.nx = 450,
.ny = 400,
.min = 0.1,
.max = 100,
.f = log};
User function for the Camera setup
bool watch (struct Camera inp) {
foreach_dimension() {
if (inp.O.x)
.O.x = inp.O.x;
camif (inp.up.x)
.up.x = inp.up.x;
camif (inp.rhs.x)
.rhs.x = inp.rhs.x;
camif (inp.poi.x)
.poi.x = inp.poi.x;
cam}
if (inp.fov)
.fov = inp.fov;
camif (inp.nx)
.nx = inp.nx;
camif (inp.ny)
.ny = inp.ny;
camif (inp.min)
.min = inp.min;
camif (inp.max)
.max = inp.max;
camif (inp.f)
.f = inp.f;
camreturn true;
}
// A ray class
typedef struct ray {
;
coord O;
coord dirint depth; // for recasted rays
} ray;
// Lights
typedef struct light {
int type; // ambient (1), Sun (2), point (3), ...
unsigned char col[3]; // Color
double I; // Intensity (at cam.poi)
; // Location of point light
coord O; // Direction of sun light
coord dir} light;
light lights[N_SKETCH]; //Reuse N_SKETCH
Material properties
Properties for matierial objects
#include "utils.h"
struct Material {
unsigned char col[3]; // prescribed color
scalar s; // color from field data
; // Corresponding colorbar ...
Colormap mapvector v; // RGB from vector
double min, max; // ... cbar range
bool linear; // Cbar interpolation;
double SPR; // Specular albedo (Blinn)
double SPexp; // Specular exponent (Blinn)
double ind; // refraction index (normal points outwards)
double T; // Transparancy (0 - 1 intended)
double R; // Reflectivity (0 - 1 intended)
bool dull; // Dull object: no light interaction just `col`
unsigned char col2[3]; // Or a gradient to col2 (not so dull!)
; // normal for `col`, col2 at n = -n1;
coord n1};
typedef struct Material material;
A few (vector) helper functions
void bias (ray * r) {
double dr = L0*1e-4;
foreach_point (r->O.x, r->O.y, r->O.z, serial)
= Delta;
dr foreach_dimension()
->O.x += r->dir.x*dr;
r}
void bias1 (ray * r) {
foreach_dimension()
->O.x += r->dir.x*L0*1e-4;
r}
bool any_col (unsigned char px[3]) {
if (px[0] || px[1] || px[2])
return true;
return false;
}
bool any_comp (coord a) {
if (a.x || a.y || a.z)
return true;
return false;
}
cross (coord a, coord b) {
coord ;
coord newforeach_dimension()
.x = a.y*b.z - a.z*b.y;
newreturn new;
}
double dot (coord a, coord b) {
double d = 0;
foreach_dimension()
+= a.x*b.x;
d return d;
}
reflect (coord in, coord n) {
coord ;
coord outforeach_dimension()
.x = in.x - 2*dot (in, n)*n.x;
outreturn out;
}
Normalization with the approximate inverse square root. Quake3 Arena method of Greg Welsh, see.
Premature optimzation is the fast inverse square root of all evil… - Donald Knuth
static inline double Q_isqrt (double l) {
double x1 = l*0.5;
union {
double f;
long long i;
} conv = { .f = l };
.i = 0x5fe6eb50c7b537a9 - (conv.i >> 1);
convfor (int i = 0; i < 2; i++)
.f *= 1.5 - (x1 * conv.f * conv.f);
convreturn conv.f;
}
void normalize_(coord * p) {
double l = 0;
foreach_dimension()
+= sq(p->x);
l = Q_isqrt (l);
l foreach_dimension()
->x *= l;
p}
#define normalize normalize_
// Thanks to scratch pixel:
// https://www.scratchapixel.com/lessons/3d-basic-rendering/
// introduction-to-shading/reflection-refraction-fresnel
refract(coord in, coord normal, double ind) {
coord (&in);
normalize (&normal);
normalize double cosi = dot (in, normal);
double etai = 1, etat = ind;
;
coord nforeach_dimension()
.x = normal.x;
nif (cosi < 0)
= -cosi;
cosi else {
= etat;
etai = 1.;
etat foreach_dimension()
.x = -normal.x;
n}
double eta = etai/etat;
double k = 1. - sq(eta) * (1. - sq(cosi));
assert (k >= 0);
= {0.};
coord out foreach_dimension()
.x = k < 0 ? 0 : eta*in.x + (eta*cosi - sqrt(k))*n.x;
outreturn out;
}
double vec_length (coord v) {
double l = 0;
foreach_dimension()
+= sq(v.x);
l return l > 0 ? sqrt(l): 0;
}
// Thanks you scratchpixel:
// https://www.scratchapixel.com/lessons/3d-basic-rendering/
// introduction-to-shading/reflection-refraction-fresnel
double fresnel(coord in, coord normal, double ind) {
(&in);
normalize (&normal);
normalize double cosi = dot(in, normal);
double etai = 1, etat = ind;
if (cosi > 0) {
= ind;
etai = 1.;
etat }
// Compute sini using Snell's law
double sint = etai/etat * sqrt(max(0., 1 - sq(cosi)));
// Total internal reflection
if (sint >= 1)
return 1.;
else {
double cost = sqrt(max(0., 1 - sq(sint)));
= fabs(cosi);
cosi double Rs = ((etat*cosi) - (etai*cost)) / ((etat*cosi) + (etai*cost));
double Rp = ((etai*cosi) - (etat*cost)) / ((etai*cosi) + (etat*cost));
return (sq(Rs) + sq(Rp))/2;
}
}
Intersections
One may compute
- ray-cell intersection
- ray-cuboid interections
- ray-plane intersection
- segment-facet intersection
- ray-triangle intersection
static inline double ray_cell_intersect (ray r, Point point, coord a[2]) {
= {x,y,z};
coord cc double in = -HUGE, out = HUGE;
foreach_dimension() {
= max(in, r.dir.x != 0 ? ((cc.x - (sign(r.dir.x)*Delta/2.)) - r.O.x)/r.dir.x : -HUGE);
in = min(out, r.dir.x != 0 ? ((cc.x + (sign(r.dir.x)*Delta/2.)) - r.O.x)/r.dir.x : HUGE);
out }
if (in >= out || out < 0)
return HUGE;
= in < 0 ? 0 : in; //The origin is in the cell.
in foreach_dimension() {
[0].x = r.O.x + in*r.dir.x;
a[1].x = r.O.x + out*r.dir.x;
a}
return in;
}
static inline double ray_box (ray r, coord bb[2]) {
double in, out;
= -HUGE; out = HUGE;
in foreach_dimension() {
if (r.dir.x > 0) {
= max(in, (bb[0].x - r.O.x)/r.dir.x);
in = min(out, (bb[1].x - r.O.x)/r.dir.x);
out } else if (r.dir.x < 0) {
= max(in, (bb[1].x - r.O.x)/r.dir.x);
in = min(out, (bb[0].x - r.O.x)/r.dir.x);
out }
}
if (in >= out || out < 0)
return HUGE;
= in < 0 ? 0 : in; //The origin is in the box.
in return in;
}
static inline double ray_plane_intersect (ray r, coord n, double alpha, coord * a) {
double ldotn = 0;
foreach_dimension()
+= n.x*r.dir.x;
ldotn if (ldotn == 0) //parallel
return HUGE;
double d = alpha;
foreach_dimension()
-= n.x*r.O.x;
d /= ldotn;
d if (d <= 0) //plane certainly not in view.
return -1;
= d > 1e4*L0 ? 1e4*L0 : d;
d foreach_dimension() {
[0].x = r.O.x + d*r.dir.x;
a}
return d;
}
static inline double ray_sphere_intersect (ray r, coord C, double R, coord * a, coord * n) {
(&r.dir);
normalize ;
coord ocforeach_dimension()
.x = r.O.x - C.x;
ocdouble det = (sq(dot(r.dir, oc)) - (sq(vec_length (oc)) - sq(R)));
if (det < 0)
return HUGE;
= sqrt(det);
det double dist = -dot(r.dir, oc) - det;
double dist2 = -dot(r.dir, oc) + det;
if (dist < 0 && dist2 < 0)
return HUGE;
else if (dist < 0)
= dist2;
dist else if (dist > dist2)
= dist2;
dist foreach_dimension() {
->x = r.O.x + r.dir.x*dist;
a->x = a->x - C.x;
n}
(n);
normalize return dist;
}
// use ray_plane_intersect?
static inline bool segment_facet_intersect (coord a[2], scalar f, Point point, coord * n) {
= {x, y, z};
coord cc [0] = mycs (point, f); //pointing outwards.
ndouble alpha = plane_alpha (f[], n[0]);
double ALP = 0, ALP2 = 0;
foreach_dimension() {
+= n[0].x*(a[0].x - cc.x)/Delta;
ALP += n[0].x*(a[1].x - cc.x)/Delta;
ALP2 }
if ((ALP2 - alpha)/(ALP - alpha) > 0.05) // 3% gap filling
return false;
double w = fabs((ALP2 - alpha)) / (fabs(ALP - alpha) + fabs(ALP2 - alpha));
foreach_dimension() {
[1].x = w*a[0].x + (1 - w)*a[1].x;
a}
return true;
}
// https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
double ray_triangle (ray r, coord t[3]) {
double eps = 1e-6;
, e2, h, s ,q;
coord e1double a, f, u, v;
foreach_dimension() {
.x = t[1].x - t[0].x;
e1.x = t[2].x - t[0].x;
e2}
= cross (r.dir, e2);
h = dot (e1, h);
a if (a > -eps && a < eps)
return HUGE;
= 1./a;
f foreach_dimension()
.x = r.O.x - t[0].x;
s= f*dot (s, h);
u if (u < 0 || u > 1.0)
return HUGE;
= cross (s, e1);
q = f*dot (r.dir, q);
v if (v < 0 || (v + u) > 1)
return HUGE;
double d = f*dot (e2, q);
if (d > eps)
return d;
return HUGE;
}
Iterators
see,
attribute {
scalar possible; // Marker for possible interesting child
vector center; // Procomputed center of vof facets? (check me!)
vector normals; // precomputed normal of vof facets/isosurfaces
scalar vofd; // Nearby vof distance field
}
#include "bwatch-iterators.h"
Color from colorbar
Quantative data can be mapped to an RGB-color code via a colorbar.
The function below is a version of colormap_color()
in output.h.
bool colormap_pigmentation (unsigned char c[3], double cmap[NCMAP][3],
double val, double min, double max) {
if (val == nodata) {
[0] = c[1] = c[2] = 0; // nodata is black
creturn false;
}
int i;
double coef;
if (max != min)
= (val - min)/(max - min);
val
else= 0.;
val if (val <= 0.) i = 0, coef = 0.;
else if (val >= 1.) i = NCMAP - 2, coef = 1.;
else {
= val*(NCMAP - 1);
i = val*(NCMAP - 1) - i;
coef }
assert (i < NCMAP - 1);
for (int j = 0; j < 3; j++)
[j] = 255*(cmap[i][j]*(1. - coef) + cmap[i + 1][j]*coef);
creturn true;
}
Lightning
Lights can be shaded by vof facets (in shading
) and
should be reflected by reflectors (in rlist
)
bool shaded (coord start, light source) {
ray l;
.O = start;
lforeach_dimension()
.dir.x = -source.dir.x;
lfor (scalar c in shading)
foreach_ray_facet_intersection(l, c)
return true;
return false;
}
double Intensity_by_reflection (coord start, light source) {
return 0;
}
void default_lights (void) {
// Ambient and sun;
[0].type = 1;
lights[0].I = 2;
lights[0].col[0] = 255;
lights[0].col[1] = 255;
lights[0].col[2] = 255;
lights
[1].type = 2;
lights[1].I = 80;
lights[1].dir = (coord){-1, -1.8, -2};
lights[1].col[0] = 255;
lights[1].col[1] = 255;
lights[1].col[2] = 250;
lights}
double absorbsion (coord a, int l) {
ray r = {.O = a, .dir = lights[l].dir};
foreach_dimension()
.dir.x *= -1;
r(&r.dir);
normalize double sl = 0;
foreach_ray_cell_intersection_volume (r, HUGE, smoke.possible) {
= {0, 0, 0};
coord mean double len = 0;
foreach_dimension() {
.x = (_a[0].x + _a[1].x)/2.;
mean+= sq(_a[0].x - _a[1].x);
len }
double val = interpolate (smoke, mean.x, mean.y, mean.z);
= sqrt(len);
len if (len > 0)
+= fabs(val)*len;
sl }
if (sl > 0)
return exp(-sl/smoke.att);
elsereturn 1;
}
double diffuse_Lambert (coord n, coord a) {
double I = 0;
int l = 0; //light
while (lights[l].type > 0) { //Fixme: No mirrored light sources yet
if (lights[l].type > 1) {
double Il = 0;
= {0,0,0};
coord ldir if (lights[l].type == 2)
= lights[l].dir;
ldir if (!shaded (a, lights[l])) {
(&ldir);
normalize foreach_dimension()
-= n.x * ldir.x;
Il double IT = lights[l].I;
if (smoke.i) {
*= absorbsion (a, l);
IT }
+= max (0, IT*Il);
I }
}
++;
l}
return I;
}
bool specular (ray r, coord n, coord a, double gloss, double alR, unsigned char c[3]) {
int l = 0;
(&n);
normalize while (lights[l].type > 0) { //Fixme: No mirrored light sources yet
if (lights[l].type > 1) {
if (lights[l].type == 2) { //Fixme: Only sun
if (shaded (a, lights[l]))
return false;
= reflect (lights[l].dir, n);
coord R (&R);
normalize double RdN = -dot(r.dir, R);
if (RdN < 0)
return false;
double I = max(alR*lights[l].I*pow(RdN, gloss), 1e-6);
for (int i = 0; i < 3; i++) {
[i] = lights[l].col[i]* min(max(log(I) - log(cam.min),0)/log(cam.max/cam.min), 1);
c[i] = min (c[i], 255);
c}
}
}
++;
l}
return true;
}
double light_intensity (coord n, coord a) {
double I = diffuse_Lambert(n, a);
int l = 0;
//ambients
while (lights[l].type > 0) {
if (lights[l].type == 1)
+= lights[l].I;
I ++;
l}
= min (max(cam.f(I) - cam.f(cam.min),0)/cam.f(cam.max/cam.min), 1);
I return I;
}
struct _get_pixel{
unsigned char c[3];
;
coord n;
coord aray r;
double SPexp;
double SPR;
};
void get_pixel (struct _get_pixel * inp) {
double I = light_intensity (inp->n, inp->a);
for (int i = 0; i < 3; i++)
->c[i] *= I;
inpif (inp->SPexp && inp->SPR) {
unsigned char tmp[3];
if (specular (inp->r, inp->n , inp->a, inp->SPexp, inp->SPR, tmp))
for (int i = 0; i < 3; i++)
->c[i] = min(inp->c[i] + tmp[i], 255);
inp}
}
Pixel color from a material hit (maybe recursive)
Mind the presidence certain properties take over others:
- Dull
- Refractive
- Fully reflective (R = 1)
- Prescribed color
- Color from scalar field data and color bar
- RGB Color code from vector field
4, 5 and 6 maybe supplemented with either 7 or 8,
- Reflection (R < 1)
- Transmission (transparancy, without refraction)
double get_color_ray(); //Prototype
trace
bool get_color_material (ray r, coord a, coord n, material mat, unsigned char px[3]) {
// Dull object?
if (mat.dull) {
if (any_comp(mat.n1) && any_comp(n)) {
(&n); normalize (&mat.n1);
normalize double l = (dot (n, mat.n1) + 1.)/2.;
for (int i = 0; i < 3; i++)
[i] = l*mat.col[i] + (1. - l)*mat.col2[i];
pxreturn true;
}
for (int i = 0; i < 3; i++)
[i] = mat.col[i];
pxreturn true;
}
// Refractive object?
else if (mat.ind) {
if (r.depth <= max_ray_depth) {
(&n); normalize (&r.dir);
normalize ray refl, refr;
.depth = refr.depth = r.depth + 1;
refl.O = refr.O = a;
refl.dir = reflect (r.dir, n);
refl(&refl.dir);
normalize double w = fresnel (r.dir, n, mat.ind);
if (w < 1) {
.dir = refract (r.dir, n, mat.ind);
refr(&refr.dir);
normalize }
bias (&refl); bias (&refr);
unsigned char ptrefl[3], ptrefr[3];
get_color_ray (refl, ptrefl);
if (w < 1)
get_color_ray (refr, ptrefr);
else= 1;
w for (int i = 0; i < 3; i++)
[i] = w*ptrefl[i] + (1 - w)*ptrefr[i];
pxif (mat.SPexp && mat.SPR) {
unsigned char tmp[3];
if (specular (r, n , a, mat.SPexp, mat.SPR, tmp))
for (int i = 0; i < 3; i++)
[i] = min(px[i] + tmp[i], 255);
px}
return true;
}
return false;
}
// Full Reflector?
if (mat.R >= 1) {
if (r.depth <= max_ray_depth) {
ray new = {.O = a, .dir = reflect (r.dir, n), .depth = r.depth + 1};
bias (&new);
if ((get_color_ray (new, px) == HUGE))
for (int i = 0; i < 3; i++)
[i] = mat.col[i];
pxreturn true;
}
return false;
}
// It must be a "tradional" object:
assert (any_col (mat.col) || mat.s.i || mat.v.y.i || mat.R >= 1 );
struct _get_pixel out;
.r = r;
out.SPexp = mat.SPexp;
out.SPR = mat.SPR;
out.n = n;
out.a = a;
outif (any_col (mat.col))
for (int i = 0; i < 3; i++)
.c[i] = mat.col[i];
outelse if (mat.s.i) {
double cmap[NCMAP][3];
scalar s = mat.s;
.map (cmap);
matforeach_dimension()
.x += L0/(1<<depth())*1e-5*r.dir.x;
adouble val = interpolate (s, a.x, a.y, a.z, mat.linear);
if (val == nodata)
return false;
colormap_pigmentation (out.c, cmap, val, mat.min, mat.max);
}
else if (mat.v.y.i) {
vector v = mat.v;
foreach_dimension()
.x += L0/(1<<depth())*1e-5*r.dir.x;
adouble val = interpolate (v.x, a.x, a.y, a.z, mat.linear);
if (val == nodata)
return false;
.c[0] = 255*min(max(val - mat.min, 0.)/(mat.max - mat.min), 1.);
out= interpolate (v.y, a.x, a.y, a.z, mat.linear);
val if (val == nodata)
return false;
.c[1] = 255*min(max(val - mat.min, 0.)/(mat.max - mat.min), 1.);
out= interpolate (v.z, a.x, a.y, a.z, mat.linear);
val if (val == nodata)
return false;
.c[2] = 255*min(max(val - mat.min, 0.)/(mat.max - mat.min), 1.);
out}
get_pixel (&out);
for (int i = 0; i < 3; i++)
[i] = out.c[i];
pxif ((!mat.R && !mat.T) || r.depth > max_ray_depth)
return true;
if (mat.R) {
unsigned char temp[3];
ray new = {.O = a, .dir = reflect (r.dir, n), .depth = r.depth + 1};
bias (&new);
if ((get_color_ray (new, temp) != HUGE))
for (int i = 0; i < 3; i++)
[i] = (1 - mat.R)*px[i] + mat.R*temp[i];
pxreturn true;
}
if (mat.T) {
unsigned char temp[3];
ray new = {.O = a, .dir = r.dir, .depth = r.depth + 1};
bias1 (&new);
if ((get_color_ray (new, temp) != HUGE))
for (int i = 0; i < 3; i++)
[i] = (1 - mat.T)*px[i] + mat.T*temp[i];
pxreturn true;
}
//this should not happen:
assert (0);
for (int i = 0; i < 3; i++)
[i] = 0; // Darkness
pxreturn false;
}
User interface
The sketch and store functions are elsewhere:
#include "sketch.h"
#undef normalize
Tests
Usage
- Visualize a dream
- Visualize a dump file
- Vortex ring colission with 4th order solver
- Turbulence at a cloud-atmosphere interface
- Untying a vortex knot
All pages using bwatch
to do
- ~
A flexible camera~
- ~
MG-accelerated~foreach_segment_3D()
ray-casting - ~
Do something ray-ish~ - ~
implement and combine more than one sketch function~ - ~
Implement a fun sketch function~ - ~
Consider lights and colours~ - Reflection of light sources
- ~
volumetric renderingplume.c~ - ~
Use bwatch for something~ - Think about optimizations to mitigate the long rendering times
- ~
Fill gaps in vof facets~
- ~
Refractive objects~ - Implement point-light sources
- Primitives cast shades
- ~
Partially reflective and transparant (alpha buffering) objects~ - ~
Gouraud shading for facets~ - ~
Isosurfaces~ - ~
RGB-code from vector field~ - ~
Parallel acceleration with OpenMP~ - MPI compatiblity (and acceleration)