Make a Cool Plasma Ball Using Voronoi Effect in Three.js

Franky Hung
Geek Culture
Published in
6 min readApr 17, 2023

--

It seems that I was quite late to the Voronoi party since the time I started learning Three.js for like 2 years ago…It looks complicated at first but the algorithm is actually pretty easy. Over time I’ve become quite the fan for trying out new patterns I learn on a Three.js sphere. This time I’m going to briefly walk you through how to do Voronoi on a sphere, with style 😎.

Code Repo and Live Demo

As usual, I started the project with my threejs-starter-template. I have to admit parcel is not the best bundler for three.js; it’s really easy to set up but there are some obvious drawbacks e.g. not able to recognize and thus bundle gltf/glb/hdr files. I’ll definitely find another bundler that overcomes these issues.

Complete project code: https://github.com/franky-adl/voronoi-sphere

Live demo:

The Setup

The three.js scene is actually just a single SphereGeometry which uses ShaderMaterial that consumes the custom shaders that I feed it with. No lighting, no textures, just a sphere with shaders. Of course there are the usuals like stats and OrbitControls.

So it all comes down to the glsl code. For the vertex shader, nothing special, just the standard code because we having nothing to change for the vertexes, only extra thing it does is to pass down the vertex positions for the fragment shader, via v_pos.

For the fragment shader, there are two parts for you to grasp. The first big chunk is the voronoi function we import from another voronoi3d_basic.glsl file, the remaining part would be what we do with the results with from the voronoi function, basically the coloring process.

Let me explain them in the following.

Explaining the Concept

So, about Voronoi…

To understand the algorithm of Voronoi, I strongly suggest the following two resources because I’ll never explain it better than them:

So, assuming you’ve read the above resources, I’ll just do a quick recap. The core of Voronoi noise is to calculate the minimum distance for each pixel to the nearest cell point which has a randomized position within its grid cell. The visual effect is simply a result of mapping the calculated distance field into greyscale or whatever colors you like.

The above examples only demonstrate the 2-dimensional case. To achieve the effect in 3D, we apply effectively the same methodology i.e. calculating minimum distances from pixels to the nearest cell points, but the cell grid now becomes 3x3x3 instead of just 3x3 because we’re in 3D.

So the new voronoi shader function(with detailed code comments) becomes:

vec2 voronoi( in vec3 x, in float time )
{
// current cell coordinates
vec3 n = floor(x);
// pixel coordinates in current cell
vec3 f = fract(x);

// initialize m with a large number
// (which will be get replaced very soon with smaller distances below)
vec4 m = vec4(8.0);

// in 2D voronoi, we only have 2 dimensions to loop over
// in 3D, we would naturally have one more dimension to loop over
for( int k=-1; k<=1; k++ ) {
for( int j=-1; j<=1; j++ ) {
for( int i=-1; i<=1; i++ )
{
// coordinates for the relative cell within the 3x3x3 3D grid
vec3 g = vec3(float(i),float(j),float(k));
// calculate a random point within the cell relative to 'n'(current cell coordinates)
vec3 o = hash3d( n + g );
// calculate the distance vector between the current pixel and the moving random point 'o'
vec3 r = g + (0.5+0.5*sin(vec3(time)+6.2831*o)) - f;
// calculate the scalar distance of r
float d = dot(r,r);

// find the minimum distance
// it is most important to save the minimum distance into the result 'm'
// saving other information into 'm' is optional and up to your liking
// e.g. displaying different colors according to various cell coordinates
if( d<m.x )
{
m = vec4( d, o );
}
}
}
}

return vec2(m.x, m.y+m.z+m.w);
}

That’s all you need to know to begin with. There are also different variants of Voronoi noise but mostly the ideas are similar.

Next up, the custom coloring code.

To get a direct visual impression of the result from the 3D voronoi function, you can try reduce the main fragment code into this:

void main() {
vec2 res = voronoi(v_pos*3., u_time*0.3);
gl_FragColor = vec4( vec3(res.x), 1.0 );
}

By applying res.x, which is essentially the distance field obtained from the voronoi function, straight as the input for the output color, you should be able to see this black and white sphere:

Already pretty by itself, right?

But we can actually do a lot more from here:

// darken by pow
vec3 mycolor = vec3(pow(res.x, 1.5));

// emphasis on blue
float blue = mycolor.b * u_bFactor;

// cut off the blueness at the top end of the spectrum
mycolor.b = blue * (1. - smoothstep(0.9,1.0,res.x));

// adjust red+greeness using pcurve such that greyness/whiteness
// is only seen at a limited range within the spectrum
mycolor.r = pcurve(mycolor.r, 4.0, u_pcurveHandle);
mycolor.g = pcurve(mycolor.g, 4.0, u_pcurveHandle);

In my code, I firstly make the blackness steeper into the black by doing a pow of 1.5, because what’s smaller 1 multiplying by itself would definitely get smaller. This increases the color contrast of the overall effect.

Next, I multiply the blue channel of the color by a factor, which defaults to 3. This largely increases the blueness of the pattern as I want to achieve a bluish effect. But since this could easily blow off from the max color value of 1.0 at the end of the spectrum, I added another step to essentially tune it down so we don’t get large patches of pure blueness which makes it look odd.

As you might wonder, dealing with the blue channel only doesn’t suffice to achieve the bluish effect because there are still values in red and green channels! In the end of my experiments, I’m most content with the usage of pcurve. I use the pcurve to tune the r and g channels to only have higher values in roughly the range of 0.6–0.8 and drops down to 0 nearing 1.0:

You can play with the function and the inputs of pcurve at https://thebookofshaders.com/edit.php#05/pcurve.frag

This explains why you see cell borders turning more white as those are areas where all color channels have high values, but then it suddenly turns very blue in further fractions of the borders when the r and g quickly descend to 0 but there’s still a high value of blueness for a short span of distance:

There you go! A cool plasma-looking voronoi sphere is thus created! You can try play with the effect to create all kinds of different colorations and fun patterns! It’s all yours 😁.

More Insanely-Cool Voronoi Shaders

--

--

Franky Hung
Geek Culture

Founder of Arkon Digital. I’m not a code fanatic, but I’m always amazed by what code can do. The endless possibilities in coding is what fascinates me everyday.