How to achieve scanning effect

@DWork, here is the demo I worked up for a scan wave effect. There are a few things going on here so let me take some time to point out some of the things to pay attention to with the demo.

I set up some parameters, some for simplicity and others for flexibility. The first was that I wanted a fairly complex scene with elevation changes in a mesh to show how the wave runs over the mesh. For simplicity’s sake, this is one mesh made from Blender’s geometry nodes, but the shader is not dependent on having a single mesh.

The other thing I wanted to achieve was to have a shader that did not rely on the UVs of the mesh at all so that it would work with any mesh. I also dropped some PBR nodes into the shader so you can see how I am hooking this into the PBR workflow.

Lastly, I wanted to give a couple of examples of wave effects, one a straight wave and the other a circular wave so you can see different approaches to the same effect.

If there were several meshes in your scene that you wanted to all have the wave shader affect, you can simply assign a clone of the base shader to each mesh and pass their individual material textures to each clone. The basis of the effect is to compare surface pixels in world space to the bounds of the mesh. In this case, I use the bounding info of the mesh to drive the extents of the effect. If you have several meshes to apply the effect to, you can either set the bounds of the wave effect manually or use a piece of mesh like a ground plane to set the extents of the effect.

You will see in the node material that the mesh bounds are being used in two paths in the shader, one for a straight wave and the other for a circular wave.

In both, we have a float with a range of 0-1 that is driving the effect. In the straight wave, it is used as the gradient on a lerp between the minZ and maxZ values which then has the mesh position.z subtracted from it. This gives us the position of the pixel in relation to the distance between min and max. I then run that value through several remap nodes. These are to set up a blend leading the position and following the position. When multiplying them together we are given a mask in the shape of the wave. I then do it a second time with a smaller mask at the lead edge to add in a brighter color at the front of the wave.

You will also see several eases called out in the shader, this is to change the linear interpolation between black and white into some nicer curved easings of value. The circular wave uses the same colors and animation as the straight wave, but makes the mask in a slightly different way.

Instead of using pixel position in relation to its place within the mesh bounds, this one needs to normalize the bounds into 0-1 values on both the X and Z axes. We do that with two remap nodes using min and max bounds to remap to 0-1. We then use a length between the two gradients to get a circular gradient. Since it’s a length of values from 0-1, the origin of the circular gradient in a corner of the bounds. If you want to place the origin in the center of the bounds, you would remap the 0-1 range into a -1 to 1 range before doing the length calculation.

Four my purposes, I chose an origin in the front corner to be behind the camera and have the wave pulse past the camera. So now that we have the gradient, we can make our mask from a simple use of smooth step nodes. I also used multiple smooth step nodes to create a gradient in front of the wave and behind to multiply together to make the final mask.

And again, there are two masks to add a second color to the lead edge of the wave to make it brighter. The animation of the wave is taken care of in the shader, but you can easily handle this in code by animating your position value in any way you want and then passing the value to the appropriate block in the shader every frame. For debug purposes, we also have a parameter that allows you to stop the animation and instead takes the position from the wavePosition parameter. You can get to it in the inspector or in the node material editor.

The last thing to note in the shader is the order I assembled the light and emission components.

I created a dark gradient to multiply with the base color to give the render more depth. I multiply the depth gradient with both the diffuse contribution and emission contribution, I then add in the specular contribution to both the diffuse and emission contributions before feeding them into a lerp driven by the wave mask. This prevents the colors from changing too much, but you could easily add them together if you like.

This node material is pretty large, but I broke it down as much as possible into frames with comments to help dissect it.

One thing I will add here is that all color nodes are set to convert to linear values to perform operations on them and then the FragmentOutput is set to convert the final color back to gamma space. Color space is something that many people coming to node material aren’t familiar with, so I recently added some documentation for it. The page is called How to Take Control of Color Space in the Preparing Assets for Babylon.js section. The shader has the correct color spaces set throughout, but the parameters are easy to miss unless you are looking for them. The documentation goes into depth about why it’s important and how to use it with node materials.

I hope this helps you get started and it should give you some ideas about what you would want to incorporate from this shader into your project. Feel free to ping back with more questions, however.

11 Likes