Writing Player Location to Fog of War in Unreal Engine: Floor Mask and Spotlight

How the player's position drives top-down fog of war in Unreal Engine: the Fog Manager writes CharacterUVLocation each tick, and the material turns it into a SphereMask spotlight intersected with a baked floor mask so light lands only on visible, walkable ground.

This is step five of the Top-Down Fog of War in Unreal Engine guide. The Fog Manager drives a dynamic instance of M_Main_Fog_Volume, and the material lerps the scene by a mask. This step builds the most important input to that mask: the player reveal, and it starts with getting the player’s position into the material.

Writing the player’s location each tick

Materials work in normalized (0 to 1) UV space, so the trick that makes the whole effect line up is converting the player’s world position to UV by dividing by the map size. Every tick the manager pushes that, plus the player’s facing and a field of view value, into the material:

void AFogManager::UpdateCharacterLocation()
{
    if (!GetLocalTracker() || !LocalTracker->GetOwner()) return;

    const FVector World = LocalTracker->GetOwner()->GetActorLocation();
    // World XY to UV (0..1). The material applies the centered offset if IsMapCentered.
    FoWMaterialInstance->SetVectorParameterValue("CharacterUVLocation",
        FVector(World.X / MapSize, World.Y / MapSize, 0.f));

    const FVector Forward = LocalTracker->GetOwner()->GetActorForwardVector();
    FoWMaterialInstance->SetVectorParameterValue("CharacterForwardDirection",
        FLinearColor(Forward.X, Forward.Y, 0.f, 0.f));

    // Cone gate: cos(half FOV). At 360 we send -1 so the cone test always passes.
    const float FOV = LocalTracker->FieldOfViewAngle;
    FoWMaterialInstance->SetScalarParameterValue("CharacterFOVCosHalfAngle",
        FOV < 360.f ? FMath::Cos(FMath::DegreesToRadians(FOV * 0.5f)) : -1.f);
}

UpdateCharacterLocation is called from Tick. GetLocalTracker just finds the locally controlled character’s tracker component (the thing that marks “this is the pawn whose vision we are drawing”); for a single-pawn game you can read the local player pawn directly.

The Tick function sending the character's normalized location to the fog material

That writes three of the parameters from the contract: CharacterUVLocation, CharacterForwardDirection, and CharacterFOVCosHalfAngle. The material reads them to draw the spotlight. The rest of this step is the two masks that turn them into the reveal: a floor mask so light only lands on walkable ground, and a player spotlight so it only lands near the player. The material combines them with a min (logical AND).

The floor mask

The floor mask keeps light off places the player cannot walk: only walkable floor is allowed to light up. Here only the dark gray areas of this test map are walkable.

A top-down test map where only the dark gray floor areas are walkable

The walkable render target

The floor mask is a black-and-white texture where white is walkable. You bake it once from a top-down capture of the level (covered in a related render target tutorial), then assign it as FloorMap. Baking it to a static texture rather than capturing it every frame is the recommended path for performance.

A render target asset that stores a black-and-white mask of the walkable floor

Sampling the mask in UV space

We sample FloorMap at the pixel’s world position converted to UV, the same worldXY / MapSize from the parameter contract (this is the World Coords UV reroute). That alignment is the whole reason everything is normalized: the player’s UV location and the floor texture share one coordinate space. The plugin runs the sampled value through an If (white above about 0.9, otherwise black) to keep the mask crisp, though a clean source texture can skip that.

A Texture Sample node reading the walkable mask in the material graph

The World in 0-1 scale group: the floor map sampled at World Coords UV, with the hero location reroute and an If node thresholding the result into a crisp black-and-white mask

Two kinds of world origin

There are two world layouts to handle, which is what the IsMapCentered parameter selects:

// AbsoluteWorldPosition.xy is the pixel's world location
float2 uv = AbsoluteWorldPosition.xy / MapSize;
uv = IsMapCentered ? uv + 0.5 : uv;   // centered worlds run -0.5..0.5, shift to 0..1
float floorMask = Texture2DSample(FloorMap, uv).r;

If your level sits entirely in positive coordinates, the divide alone lands in 0 to 1. If the origin is centered, the world runs from a negative to a positive value, so we add half (0.5 in UV) to shift it up into the 0 to 1 range first.

The material graph handling both a positive (cornered) world origin and a centered world origin

The full node setup converting Absolute World Position to UV for both a cornered (0 to map size) and a centered (minus half to plus half) world origin, with a switch selecting between them

Output to a Named Reroute

We output the floor mask to a Named Reroute (“portal”) so the combine section can read it without dragging a wire across the whole graph. Do the same for every mask; it keeps a graph this size readable.

The floor mask routed into a Named Reroute output node

The player spotlight

This is the circle of vision around the player. It reads the parameters the manager just wrote: CharacterUVLocation, CharacterVisibilityRadius, and (for a facing cone) CharacterForwardDirection and CharacterFOVCosHalfAngle.

The Fog Manager C++ sending map size and visibility radius to the material

The base of the spotlight is a SphereMask. Two of its inputs are Named Reroutes the graph builds once: Hero Location (the CharacterUVLocation parameter) and World Coords UV Centered (the pixel’s position in the same raw worldXY / MapSize space, so the two line up). The radius is normalized too, CharacterVisibilityRadius / MapSize, with a hardness around 90 for a soft edge:

float radiusUV = CharacterVisibilityRadius / MapSize;
float sphere   = SphereMask(WorldCoordsUVCentered, HeroLocation, radiusUV, /*hardness*/ 90);

The player spotlight nodes reading the manager parameters to draw a white sphere at the player

One gotcha worth calling out: the floor mask samples World Coords UV (offset into 0 to 1 so it matches the floor texture), while the sphere mask and the cone use World Coords UV Centered (raw worldXY / MapSize, matching Hero Location). Same world, two conventions, each used where it is convenient. Mix them up and the spotlight drifts off the player.

Restricting the spotlight to a facing cone

If you want directional vision instead of a full circle, gate the sphere by a field of view cone. Take the direction from the player to the pixel, dot it with the player’s forward vector, and compare against CharacterFOVCosHalfAngle with a step. Because the manager sends -1 when the FOV is 360, the cone test passes everywhere and you are back to an omnidirectional circle for free:

float2 toPixel = normalize(WorldCoordsUVCentered - HeroLocation);
float  facing  = dot(toPixel, normalize(CharacterForwardDirection.xy));
float  inCone  = step(CharacterFOVCosHalfAngle, facing);  // 1 inside the cone
float  spotlight = saturate(sphere * inCone);

The full player spotlight in the material: a SphereMask at the hero location gated by the FOV cone (a dot product fed into a step), with the occluder stencil subtracted on the right

Output the spotlight through its own Named Reroute. The shipping material does one more thing to it, subtracting vision occluders so cliff faces do not glow, which is covered with the master material.

Combining floor and spotlight

Now combine the two masks. Intersect the spotlight with the floor (min) so light never spills onto unwalkable areas, then clamp:

float reveal = min(floorMask, spotlight);
float mask   = saturate(reveal);

A min node combining the floor mask and the player spotlight in the material graph

The saturate keeps the mask in the valid 0 to 1 range before it drives the final lerp from the master material.

A saturate node clamping the combined mask between 0 and 1

The result

Drop the Fog Manager into the level and you have the core reveal: the whole map is darkened, and a soft circle of walkable floor lights up and follows the player.

The finished fog of war effect: a spotlight lighting the floor around the player in the Unreal editor

What’s next

That is one vision source: the player. Next, add stationary ones: writing beacons to the material reveals area even when the player is away. See the guide hub for the full reading order.

Frequently asked questions

How is the player's location written to the fog material?
The Fog Manager reads the local pawn each tick, divides its world XY by the map size to get a 0-to-1 UV, and sets it as CharacterUVLocation on the dynamic material instance, alongside the forward vector and an FOV cosine.
Why sample the floor mask in UV space?
So the texture, the player's position, and the spotlight all share one 0-to-1 coordinate space. Everything is normalized by dividing world XY by the map size, which is why the masks line up.
How do I make the spotlight directional instead of a full circle?
Multiply the sphere mask by an FOV cone: dot the direction from the player to the pixel with the player's forward vector and compare to cos(half FOV) with a step. Send -1 for a full 360-degree circle.
Why combine the floor and spotlight with a min node?
min is a logical AND: a pixel only lights up where both the floor mask and the spotlight are white, so the player has to be standing on walkable floor for it to light.