Short description
What you see in the image is a render of the complex Julia set we
all know and love extended to the 4th dimension and projected back into
3D space. I was always fascinated of Julia sets and fractals and wanted
to create a visualization myself. Originally, we wanted to go for a scene similar to scenes in this video.
We rendered the image raymarching the signed distance function
of the Julia set. That means that - instead of computing the
intersection of ray with the set - we start from the origin and take
steps in the direction of the ray until we reach a point that is
contained in the set. We are using a distance estimator to adapt the
step size and speed up the intersection computation.
The normal at the intersection point is computed by a simple finite difference method.
For more math read this
wonderful article by
Inigo Quilez.
The scene is just a simple plane with a mirror material, two area
lights (one pink and one cyan) and the Phong shaded fractal solid.
The final render was done with 300 times supersampling and took 19
minutes and 55 seconds on 8 cores.
The animation in the background has 30 frames and only uses about 40 times supersampling since we did not have
time to render 30 frames at full 300 times supersampling.
Implementation
Since the renderer takes quite a long time to render a 1920x1080 image, we decided to parallelize it since
raytracing is an "embarrasingly parallel" problem. Fortunately, this is easily done in C++ 2017 standard by
using execution policies:
std::for_each(
std::execution::par,
pixels.begin(), pixels.end(),
[&](const std::pair<int, int> &px) {
img(px.first, px.second) = renderPixel(px.first, px.second);
});
We implemented the possibility to render both square and cubic Julia sets, but our final render displays a
square Julia set.
float squareSdf(const Point &p, const Float4 &c) {
Float4 z{p.x, p.y, p.z, 0};
float md2 = 1.0;
float mz2 = qLengthSqr(z);
for (int i = 0; i < NUM_ITER; ++i) {
md2 *= 4.0 * mz2;
z = qSqr(z) + c;
mz2 = qLengthSqr(z);
if (mz2 > 256)
break;
}
float d = 0.25 * std::log(mz2) * std::sqrt(mz2 / md2);
return d;
}
The intersection was done by iteratively taking small steps along the
direction of the ray, calculating the signed distance function and
breaking if the distance is small enough. Once we have the hit point
on the surface of the Julia set, we calculate the normal by means of
a simple finite difference method:
Vector Fractal::computeNormal(const Point &p) const {
float ex = 0.5773 * 0.00025;
float ey = -0.5773 * 0.00025;
Vector exyy = Vector(ex, ey, ey);
Vector eyyx = Vector(ey, ey, ex);
Vector eyxy = Vector(ey, ex, ey);
Vector exxx = Vector(ex, ex, ex);
return (exyy * sdf(p + exyy)
+ eyyx * sdf(p + eyyx)
+ eyxy * sdf(p + eyxy)
+ exxx * sdf(p + exxx)).normalize();
}
The generated image is located in the build folder named "fractal.png"
About
Members:
- Daniel Gusenburger
- Siddharta
References: