top of page

UV Blacklight Shader

While playing Phasmophobia, a popular ghost hunting game, I was inspired by their uv blacklight effect and wanted to try my own hand at it. The version I came up with tracks the player's flashlight position and viewing angle, and then uses that information to recreate a world-space cone mask that materials can use to drive opacity.

First I created a Material Parameter Collection and added some parameters to it. The important ones here are the two vector parameters that will hold the flashlight's position and forward direction that it's facing. I also added a few other scalar parameters to customize the effect, but those are not necessary.

UV_MPC.png

In my character blueprint I grabbed a reference to my lightsource, and assigned the position/forward vector to the global MPC values I created. In this case, I'm using Event Tick to drive this because I'm lazy, but in an actual game implementation we'd want a custom function in code. I also am setting the Flashlight On value using the right mouse button, since that toggles my flashlight turning on and off.

UV_FlashlightInfo.png
UV_FlashlightOn.png

That should be all we need to do on the blueprint side of things; the rest is done in the material itself. I created an Unlit Masked material and have a basic setup using an emissive color. The opacity is driven by a combination of the texture that we want to appear, and also the cone mask (called MF_UV) that we will create. And the opacity is dithered for a smooth fade in/fade out.

UV_mat1.png

Now for the most complicated part: building the cone mask. I created a material function called MF_UV, and I added all the MPC flashlight parameters to it. In order to build the cone mask, we need to know a few things: what is the cone's angle, how far does the cone extend, and whether the surface is facing toward the light and should be lit (optional).

UV_mat2.png
  • In order to create the angle of visibility we can use the Dot Product. We subtract the flashlight's position from the object's vertex position in order to create a vector pointing from flashlight to object. We can then use the Dot Product to compare this vector to the flashlight's forward position, which gives us a value between -1 and 1 depending on how similar the vectors are. Using the Cone Angle parameter, we can do some math to clamp the angle mask and remap it to a smooth 0-1 range. 

  • To determine how far the cone extends, I originally used the Distance node to check the distance between the flashlight's position and the world position of the object's pixels. I ended up switching that math to be the dot product between the forward vector and the un-normalized pixel vector. This is because an un-normalized dot product is actually a cheap approximation of distance, and since we are already using the vectors elsewhere we save a few instructions by doing this. Dividing by the Attenuation parameter gives us our distance mask.

  • And finally, if we only want surfaces facing the lightsource to be illuminated, we can compare the vertex normal vector of the object to the spotlight's forward vector (again using dot product) to create one final mask. In a single player game, this part is unnecessary because the player can only see the side of the mesh that they are pointing the flashlight toward. They cannot see the back side of any object. This is more relevant for a multiplayer game, where one player might see parts of an object that another player cannot. 

UV_Final.png
Future Performance Optimizations

The current setup I have is great for a singleplayer game, because it's relatively cheap shader math being done on decals. But there's definitely room for improvement:

​

  • We would definitely want to move all the blueprint code to C++ to reduce those overhead costs

  • If we decide to leave the parameters on Tick, then we'd want to modify the Tick Rate to be fairly infrequent (like 4 per second or something)

  • The biggest weakness of this approach is regarding multiplayer games. If each player has a flashlight and we're tracking 4+ flashlights, then we have to run the shader math 4+ times on every object that needs the mask. Using decals helps minimize the amount of shader costs, but it may be cheaper to set up a custom render pass for the flashlights. That way the shaders can directly sample the render pass and use it as a mask without needing to generate individual cone masks using math.

  • Instagram
  • Vimeo
bottom of page