WebGL Grim Reaper demo

Oleksandr Popov
6 min readFeb 23, 2022

--

Screenshot from WebGL demo

A couple weeks before Halloween 2021 I browsed Sketchfab and encountered a cool 3D model of Grim Reaper by 3DRT. It has a reasonable polycount, a set of different colors and smooth animations. So the decision was made to create a Halloween-themed live wallpaper with this model. However, I was not able to finish it before Halloween because I gradually added some new effects and features which took quite some time to implement and then tweak.

You can find a live web demo here, and for persons sensitive to flickering lights a version without lightning is here. You can interact with it by clicking the mouse on screen — this will change animation. Also you can enter free-camera mode which uses WASD navigation by pressing the Enter key.

As usual, source code is available on Github.

And of course you can get an Android live wallpaper app.

Scene composition

Scene is pretty simple so it doesn’t require any sorting of objects — carefully chosen hardcoded render order achieves minimal overdraw:

Scene rendering stages

First, opaque (cloth is alpha-masked so it is also opaque) geometries are rendered. These animated objects use vertex animation with data stored in FP16 textures, so WebGL 2 is required for the demo.

After rendering opaque geometries, writing to depth is disabled with glDepthMask(false) and then transparent effects — smoke, dust and ghosts are drawn over them with blending. Sky is also drawn at this stage. Because it is the most distant object, it doesn’t have to contribute to depth — it is basically treated as a far clipping plane.

Effects

That’s where most of the time was spent — thinking of, creating, tweaking and rejecting various effects for a really simple scene with literally a single character in it.

Every time I had an idea on how to improve a look I added it to the Trello board. Then I had some time to think about it — how will it fit the scene, how to implement it, etc. So here is a breakdown of all used effects.

First, soft particles are added to the reaper. Half of them rise upwards, half of them sink down from roughly the center of the reaper model which fluctuates a little depending on animation. Of course to get the best visual appearance soft particles are used, hence the depth pre-pass. You can read about implementation of soft particles in one of my previous articles.

Then some flickering dust is rendered. You may notice that its brightness is synchronized with lightning strikes — usually dust slowly fades in and out but at lightning strikes it is more visible.

As a final touch, a rather heavy vignette is applied. This effect blends nicely with the gloomy atmosphere, helps to draw attention to the center of screen and to visually conceal the bland void in the corners of the screen.

There are still a couple of effect ideas noted in my Trello board but I think that adding them will only clutter the scene without adding any more noticeable eye candies.

Sky shader

Sky is used to fill in the void around the main character. To add some dynamics and movement to these empty parts of the scene it is rendered with a shader which applies simple distortion and lightning to static clouds texture.

Let’s analyze the shader code. It combines three simple effects to create a dynamic sky:

  1. It starts with applying color to rather bland-looking greyscale base sky texture:
Colorized sky

2. Then, waves from a small distortion texture are applied (a similar but more pronounced effect can be used for water ripples). Effect is subtle but does noticeably improve overall look:

Distortion applied

3. And the final touch is lightning. To recreate somewhat realistic looking lighting which cannot get through dense clouds but shines through clear areas, brightness is increased exponentially — darker parts will get very little increase in brightness while bright areas will be highlighted. Final result with all effects combined looks like this:

Combined sky effects

Timer for the lightning strikes is a periodic function of several sine waves combined, clamped to range [0…2]. I’ve used a really handy Desmos graphing calculator to visualize and tweak coefficients for this function — you can clearly see that the “spikes” of positive values create short periodic randomized bursts:

Lightning intensity graph

Additionally, the sky sphere slowly rotates to make the background less static.

Ghosts shader

Ghostly trails floating around the grim reaper are inspired by this Unreal Engine 4 Niagara tutorial — https://www.artstation.com/artwork/ba4mNn.

Initial idea was to use a geometry in a shape of cutout from the cylinder side and rotate it around the center of the reaper model. However, my brother created a shader for a more flexible approach to use a single geometry which can be rotated at arbitrary radius and stretched to arbitrary length.

To achieve this, it changes the geometry of the original mesh in the vertex shader. The shader modifies X and Y coordinates of the input model, bending them around the circle of given radius. Z coordinate is not getting additional transformations. It is responsible for scaling the final effect vertically. (World space is Z-up). Shader is tailored to work with a specific model — a tessellated sheet in the XZ plane (all Y coordinates are zero):

Ghost geometry

Later, geometry was optimized to tightly fit our sprite texture in order to reduce overdraw:

Ghost geometry optimized

Based on the math of chord length, the X and Y coordinates of bent model are:

x = R * sin(theta);
y = R * cos(theta);

where theta = rm_Vertex.x / R, and R is a bend radius. However, theta is calculated differently in the shader:

float theta = rm_Vertex.x * lengthToRadius;

lengthToRadius value is a uniform, but it is not just a reciprocal of R — we can pass values greater than 1/R to get effect length scaled (because it essentially is a pre-multiplication of rm_Vertex.x).
This minor change is done in order to eliminate redundant uniform-only math in the shader. Preliminary division of length by radius is done on the CPU and this result is passed into the shader via lengthToRadius uniform.
I’ve tried to improve this effect by applying displacement distortion in fragment shader but it appears to be virtually unnoticeable in motion. So we kept the original simpler version with static texture, which is also cheaper for the GPU.

Reduced colors filter

Not implemented in the web version, but present in Android app is a reduced colors post-processing. This gritty effect perfectly fits the overall atmosphere and adds a right mood to the scene. It is implemented not as a separate post-processing render pass but is done in the fragment shader, so rendering is still essentially single-pass.

Reduced colors filter

It is based on code from Q1K3 WebGL game https://github.com/phoboslab/q1k3, and I highly recommend to read a blog post about making of seemingly impossible Q1K3 — https://phoboslab.org/log/2021/09/q1k3-making-of.

Textures compression

Android live wallpaper targets OpenGL ES 3.0+ and uses efficient ETC2 and ASTC compressed textures. However, WebGL demo is optimized only for the fastest possible loading time. I really hate when some simple WebGL demo takes forever to load its resources. Because of this, a decision not to use hardware compressed textures was made. Instead, textures are compressed as lossy WebP. Total size of all assets including HTML/CSS/JS is just 2.7 MB so it loads pretty fast. Recently, our mountains demo has also been updated with smaller resources but it is still way larger than the Reaper one — it downloads 10.8 MB of data on initial load.

--

--

Oleksandr Popov

Front-end developer making 3D live wallpaper apps for Android.