Approach
An outline shader for a multiplayer game, highlighting objects by generating dynamic outlines — a purely visual effect that leans on GPU parallelism. The project started from just an idea; everything about it had to be built from scratch, with a requirement that the look be adjustable via material-instance parameters, without touching code.
A tutorial for an older Unreal Engine version, implementing dynamic outlines as a material using both depth and normal-vector edge detection, was the starting reference. Depth-based edge detection worked without issues — normal-based detection turned out to be the hard part (covered below).
Stylization: random width and per-player color
Two further requirements came out of team feedback: a randomized outline width for a more hand-drawn look, and distinct outline colors per player.
For width variation, Perlin noise supplied pseudo-random values, and Fractal Brownian Motion — using two octaves, since more detail wasn’t visible on the outlines anyway — added fine, adjustable transitions on top, all exposed as parameters.
For per-player colors, a color-wheel widget was added so a designer could pick colors visually rather than working with numeric values directly; those picks were then converted to numeric values and assigned per player.
Notable problems solved
Assignment order
Colors weren’t always assigned correctly. Debugging traced it to
OnPostLogin — which fires when a client connects — happening before
that client’s pawn (the object the color actually needed to be assigned
to) existed yet. Reordering the assignment logic fixed it server-side,
though not yet for clients.
Network replication
The server assigned colors correctly, but clients had no way to know which colors were already taken by other clients. The fix used Unreal’s replication system: available colors were tracked in an array read from GameState; when a client connected, its Mode looked up a free color from that array and assigned it to the client’s PlayerState; once set, that value replicated out to every other client’s PlayerState, which is what each client then used to display the correct outline color per player.
Forward shading on mobile
Normal-based edge detection initially worked in Unreal’s deferred renderer, which provides a World Normal Buffer with every rendered surface’s normal data. But the game targets mobile, so forward shading is used instead for performance — and forward doesn’t provide a World Normal Buffer. The choice was between accepting depth-only outlines, or reconstructing the normal data manually.
Early attempts at reconstructing normals produced clearly wrong results — outlines appearing on the front faces of spheres, for instance. Research into “screen-space approximated normals” led to a working approach: computing normals from the depth differences between five surrounding points sampled in a cross pattern.
Since computing normals this way — every frame, rather than reading them from a buffer — adds GPU work (visible mainly as an increased fragment shader count), this was checked against the performance profiler. The impact turned out to be immeasurable; the game stayed smooth on mobile. The result isn’t identical to the deferred/buffer-based version, and some bugs remain at very high outline widths (not really a supported use case), but it was a solid compromise.
Result
Despite the blockers, the outline shader shipped successfully — giving players a clearer way to spot and tell apart other players on both sides of a match.
Cross-engine implementation
Cross-engine implementation
Unreal Engine
Unreal implementation here.