Historically, the web demo was created first — it was used as a prototyping playground to compose a scene and to fine-tune shaders. Also, this really helps in sharing work between a team of two people without necessity to learn Android Studio for both. And when everything was polished and looked good enough, an Android app was created quite fast based on the web demo code. Porting code to Android is a quite straightforward and easy process because our WebGL framework has the same method signatures as the framework used in Android apps.
Scene is quite simple and contains just six objects — terrain, sky, dust particles, sun, birds, and palm trees.
To examine how objects are rendered, you can take a look at
drawScene() method in DunesRenderer.ts — first we render depth map to texture (this is needed for soft particles), then render on-screen objects in front-to-back order (first closest and largest objects, then distant) to efficiently utilize z-buffer culling.
Terrain in the scene is represented as a single square tile. The base for terrain is this model purchased on CGTrader. Its polycount is reduced to 31k faces in order not to split geometry and to draw it with a single draw call. This polycount produces a reasonably good quality. However, its area is not quite large enough to create a feel of infinite sand desert — when the camera is placed slightly above terrain boundaries of square terrain its limits are clearly visible:
Apparently this reduces the range of camera movement and creates an unwanted feeling of terrain “floating” in space. To eliminate this effect and improve the immersiveness of the scene we use a technique called “terrain skirt”. We learned about it from this great GDC talk about terrain in Halo Wars. You should definitely watch the whole video as it explains a lot of other interesting and unique techniques which might come in handy. The idea behind this terrain skirt is to render the same tile at the edges of tile but mirrored away from the center of the scene. This significantly expands the area of terrain. This screenshot shows all 8 additional tiles rendered (with additional gaps to separate tiles):
You can see a mirroring of tiles at the edges where duplicate tiles connect with the main one but it is not noticeable in the final app because the camera is placed only within the main tile avoiding looking at those edges directly. We render additional tiles 1.5 times larger than original ones, effectively increasing perceived dimensions of terrain 4 times. This short clip showcases how final extended terrain looks with and without skirt:
As you can see, this simple trick creates a vast, seemingly endless terrain stretching up to horizon with very little effort and reuses existing geometries.
For dust effect soft particles are used. You can read more about this technique in our previous article — https://dev.to/keaukraine/implementing-soft-particles-in-webgl-and-opengl-es-3l6e.
The only object rendered to a depth texture for soft particles is the main terrain tile because that’s the only geometry particles intersect with. To make this rendering faster, the simplest fragment shader is used to render this object instead of the complex one used to render the on-screen terrain.
To simulate the effect of wind creating sand waves on the dunes surface we’ve developed a quite complex shader. Let’s take a look inside of it. Please note that while we will explain GLSL code of shader, the generic techniques and approaches used in it can also be applied to recreate similar material in Unity/Unreal engines.
The code of the shader can be found in DunesShader.ts. Let’s analyze it.
Terrain uses a quite large texture — 2048x2048 for web demo, and up to 4096x4096 in Android app. Obviously it takes quite some memory so to efficiently use it, some tricks were used. The main diffuse color for dunes is actually stored as a single-channel grayscale value in the red channel of terrain texture. Actual color of sand is specified by
uColor uniform which is multiplied by grayscale diffuse value. The other 2 channels contain lightmaps for high sun (day and night) and low sun (sunrise and sunset). Since it is not possible to use uniforms for accessing texture data, two versions of shader are compiled for two lightmaps. Final diffuse color is multiplied with shadows color.
Next, let’s take a look at how the moving wind effect is created. You may notice that it is different for windward and leeward slopes of dunes. To determine which effect to apply to which slope, we calculate blending coefficients from surface normal. These coefficients are calculated per vertex and are passed into the fragment shader via
vSlopeCoeff2 varyings. You can uncomment corresponding lines in fragment shader to visualize windward and leeward parts with different colors:
Both slopes use the same texture applied to them but windward one is more stretched. Texture coordinates for both slopes are also calculated in vertex shader to prevent dependent texture reads. Wind movement is done by adding offset to texture coordinates from
The next important thing to get a realistic result is to apply atmospheric fog. For performance reasons, we use a simple linear fog which is calculated in the vertex shader. Fog range is controlled by two uniforms —
fogDistance and value to be used in the fragment shader is calculated and stored in
vFogAmount varying. Fragment shader applies fog color from
uFogColor uniform based on the value of this varying.
Fog color is adjusted for far terrain edges to blend with sky texture. And the sky texture is also edited to have a distant haze of the same fog color in places where it should blend with the terrain.
Even though the overall terrain texture is quite large, it covers a large area and therefore still not detailed enough for close-ups. To make dunes less blurry and more realistic when observed from the ground we apply a detail texture to it. It is a small 256x256 texture which has 2 different sand ripples patterns in 2 channels for different slopes. Detail texture can either darken or lighten diffuse color. To achieve this, first we subtract 0.5 from the detail color so it can have negative value, and then this value is added to the final color. This way, 50% gray color in detail texture doesn’t affect diffuse color, darker values will darken it and brighter values will brighten color. Detail texture is applied the similar way as the fog — it has two uniforms to adjust cutoff distance where detail texture is not needed. You can uncomment a line in fragment shader to visualize detail texture range in red channel:
You can see a live demo page here. It is interactive — you can click to change time of day. And on desktop to examine the scene from any arbitrary position you can go into free flight mode by pressing the Enter key. In this mode, to rotate the camera hold the right mouse button and to move use WASD keys, Space to go up and C to go down. Hold Shift while moving to accelerate.
Full source code is available on GitHub, if you are interested in recreating similar effects you can clone and use it for your needs — it is licensed under permissive MIT license.