Outlines
A line around an object's silhouette (and sometimes its inner edges). Used for toon/cel styles, and for gameplay: selection, interactables, x-ray through walls.
Two families: geometry (draw extra geometry per object) and screen-space (find edges in buffers after rendering). Style outlines want per-object control and inner lines; gameplay highlights want a clean, constant-width silhouette on anything.
Baseline: a thick jump-flood silhouette (5 px, ink) over thin edge-detected inner lines (1 px, softer ink). The contrast between the two weights is what reads as "drawn".
At a glance
| Technique | Looks like | Cost | Best for |
|---|---|---|---|
| Inverted hull | Thick, per-object line, can vary width | low | Toon characters |
| Edge detection | Lines on every depth/normal/colour break | low–med | Whole-scene ink/sketch styles |
| Mask dilation | Soft or hard glow around a selected object | low–med | Thin selection highlights |
| Jump flood | Crisp, constant-width, any width | med | Toon silhouettes, gameplay highlights |
| Occluded parts (x-ray) | Outline or hatched fill behind walls | low | Allies, objectives, interactables |
| Fresnel rim | Glowing edge inside the silhouette | very low | Cheap "hit"/"selected" glow |
| Authored lines | Hand-drawn inner lines that don't swim | authoring | Anime / fighting-game look |
Inverted hull
- Looks like: A solid dark shell behind the mesh; width can be varied by vertex colour.
- How:
- Draw the mesh a second time, front faces culled.
- Push vertices out along their normals (in clip/view space for constant screen width).
- Output a flat colour.
- Strong: Trivial, per-object, width/colour painted per vertex, works on skinned meshes, no full-screen pass.
- Weak: Hard normals split the hull (gaps at corners) → need smoothed normals baked into a spare channel. Open meshes, flat quads and intersecting parts gap or double. Only outer silhouette + some creases. Doubles draw calls.
- Needs: Smoothed normals (baked to vertex colour/UV), a second pass or material per object.
- Cost: low runtime (extra draw per object); med authoring (normal baking, width painting).
- Variants:
- Width by distance / by vertex colour for tapered, brush-like lines.
- Stencil-masked shell: write stencil in the main pass, draw scaled shell where stencil ≠ ref (fixes the inside but not open meshes).
- Seen in: Guilty Gear Xrd, Genshin Impact, Jet Set Radio, most toon shaders.
- Reference code: inverted-hull.js

Zoomed 6×: with split (hard) normals each face's shell moves out on its own and the corner opens.
Without smoothed normals the hull splits at hard edges (box corners, robot panels).
Edge detection (post-process)
- Looks like: Ink lines on silhouettes, creases and material boundaries, across the whole scene.
- How: Render depth, normals and an ID buffer alongside colour. One full-screen pass compares each
pixel to its neighbours (±width px); each term catches a different edge, and any term over its threshold draws ink:
- Depth: silhouettes and depth steps. Use the Laplacian of inverse depth, which is 0 on any plane, so floors at grazing angles don't fire. A plain depth difference needs distance/normal-scaled thresholds instead.
- Normal: creases where depth is continuous (box edges, bevels):
1 - dot(n, n_neighbour). - ID: boundaries between objects/materials that touch at the same depth and normal (painted bands, a hand resting on a body, a box on the floor). Any per-pixel ID works: object ID, material ID, stencil value.
- Strong: One pass for the whole scene, catches inner lines, no per-object work, and lines stay 1–2 px crisp.
- Weak: Thresholds are scene-dependent; width is limited by the kernel (wide lines get blobby and costly); lines swim on dense or noisy geometry; a line can't be restyled per object without the ID buffer.
- Needs: Depth, a normals target (MRT), an ID target; the camera's near/far to linearise depth.
- Cost: low–med (one full-screen pass, ~12 taps per term).
- Variants:
- Hand-authored IDs / line masks: artists paint IDs (or a line-weight channel in vertex colour or a texture) to force lines where geometry has none (panel seams) or suppress them where it has too many (merge parts under one ID). This is how most stylised games get control back from a purely automatic filter.
- Colour/luminance edges for a sketchy look; line width by depth for atmospheric perspective.
- Pair with a jump-flood silhouette for thick outer lines + thin inner lines (the toon baseline).
- Seen in: Borderlands, Return of the Obra Dinn (plus dithering), Moebius-style shaders.
- Reference code: edge-detection.js, ID buffer in passes.js (
assignMaterialIds)

What each buffer and term contributes:
| Normals | Material IDs |
|---|---|
![]() |
![]() |
| Depth term: silhouettes, not floors | Normal term: creases |
![]() |
![]() |
| ID term: material boundaries | Close-up (5×): 1–2 px lines |
![]() |
![]() |
Where the naive depth test fails: a low camera looking along the floor.
| Naive depth difference | Inverse-depth Laplacian |
|---|---|
![]() |
![]() |
Mask dilation (blur / kernel)
- Looks like: Selection outline or glow around one object; soft if blurred, hard if thresholded.
- Same goal as jump flood, brute-force algorithm: both answer "is this pixel within w px of the selection?". Dilation searches every neighbour (≈(2w+1)² taps, one pass); jump flood finds the nearest silhouette pixel in log2(w) passes and returns a real distance. Dilation wins for 1–3 px; jump flood wins above that and enables AA/bands.
- How:
- Render the selected objects flat into a mask.
- Dilate: blur the mask or take the max over a kernel.
- Outline = dilated mask minus original mask; composite.
- Strong: Simple, per-object selection, works on any mesh, can go through walls (render mask without depth test).
- Weak: Cost grows with width (kernel size); wide lines get lumpy corners; blur gives uneven width.
- Needs: A way to render a subset of objects into an offscreen target (layer / render list).
- Cost: low for thin lines, grows ~linearly (separable) or quadratically (box) with width.
- Variants: Separable Gaussian glow; stencil-only (no mask texture) variant.
- Seen in: Unity/Unreal editor selection, Left 4 Dead item glow.
- Reference code: mask-dilation.js, mask in mask.js
| Hard (max over disc) | Glow (Gaussian, additive) |
|---|---|
![]() |
![]() |
Zoomed 8×: a max-over-disc band is binary, so its edge is stair-stepped (compare jump flood below).
Jump flood (distance field)
- Looks like: Crisp, anti-aliased, constant-width line of any width; can be shaped (inked edge, wobble, gradients).
- How:
- Render the selected objects into a mask.
- Seed: each mask pixel stores its own position.
- Jump flood: log2(width) passes; each pixel checks 9 neighbours at ±step, keeps the nearest seed, step halves.
- Now every pixel knows its distance to the silhouette → shade bands by distance.
- Strong: Width is almost free (log cost); true distance lets you do AA, multi-band, wobble, fades. Works on skinned, open, overlapping meshes — it only sees the silhouette.
- Weak: Several full-screen passes and a float RG target; silhouette only (no inner lines); overkill for 1–2 px.
- Needs: Offscreen mask render, RG float (or RG16) ping-pong targets.
- Cost: med (≈ 2 + log2(width) small full-screen passes, only while something is highlighted).
- Variants:
- Ink edge + noise-driven wobble for a hand-drawn look.
- Seed from the visible part or the whole silhouette to hide or show the outline behind occluders (see Occluded parts).
- Seen in: wide selection outlines in modern editors and tools.
- Reference code: jump-flood.js, mask in mask.js
| 6 px band + ink edge | 20 px, one extra pass |
|---|---|
![]() |
![]() |
Zoomed 8×: the outer edge is anti-aliased from the distance value; the inner edge follows the aliased mask (render the mask with MSAA or supersampling if that matters).
Occluded parts (x-ray)
Not an outline technique itself, but a layer that works with any mask-based one (jump flood, dilation).
- Looks like: Selected objects stay readable behind walls, either as an outline or as a tinted, hatched "ghost".
- How:
- Render the selection mask with two channels: whole silhouette (no depth test) and visible part
(fragment depth vs. scene depth). Blend with
maxso overlapping parts don't overwrite each other. hidden = whole AND NOT visible.- Modes: Hide (outline seeds from the visible part only), Outline (seeds from the whole silhouette, drawn over occluders),
Ghost (Outline + fill
hiddenwith a tint and scrolling pattern). - Anchor the pattern to the object's projected centre and scale it with distance, so it moves with the object instead of swimming.
- Render the selection mask with two channels: whole silhouette (no depth test) and visible part
(fragment depth vs. scene depth). Blend with
- Strong: Cheap once the mask exists; one mask serves outline and fill.
- Weak: Self-occlusion counts as "visible" (only other geometry hides); per-object anchoring needs per-object data.
- Needs: Scene depth texture before the mask pass.
- Cost: low (one full-screen pass).
- Seen in: Left 4 Dead teammates, Overwatch/Valorant ally outlines, Batman: Arkham "detective mode".
- Reference code: mask in mask.js, fill in xray.js
| Hide | Outline | Ghost |
|---|---|---|
![]() |
![]() |
![]() |
Fresnel rim
- Looks like: The edge of the object glows from inside; no line outside the silhouette.
- How: In the object's shader,
rim = pow(1 - saturate(dot(N, V)), k); add or lerp to a colour. - Strong: Nearly free, no extra passes, good for hit flashes and "selected" states.
- Weak: Not a real outline: fails on flat faces facing the camera, thickness varies with curvature.
- Needs: Access to the object's shader.
- Cost: very low.
- Variants: Rim lighting (masked by light direction) for toon shading.
- Seen in: Hit flashes in most action games, Fortnite/Overwatch highlight states.
- Reference code: fresnel-rim.js

Authored lines
- Looks like: Hand-drawn inner lines (creases, muscles, clothing seams) that stay stable in motion.
- How: Lines painted into textures aligned to UVs laid out straight along lines (e.g. "Motomachi lines"), or extracted as geometry (crease/feature edges) offline.
- Strong: Full artistic control, no swimming or threshold noise.
- Weak: Heavy authoring; doesn't adapt to silhouette — pair with inverted hull.
- Needs: Artist pipeline, special UV layout.
- Cost: ~0 runtime, high authoring.
- Seen in: Guilty Gear Xrd, Dragon Ball FighterZ.
In game design
Outlines do two different jobs, and most games use several kinds at once, each with its own rules:
| Layer | Purpose | Typical look | When it's on |
|---|---|---|---|
| Line art (art style) | Readability and style: shapes read clearly, the world looks drawn | Dark ink silhouette + thinner inner lines, on everything | Always; part of the look, like lighting |
| Focus / interactable | "You can use this" / "this is what you're aiming at" | Bright, wider band (jump flood), often animated in | Only on the current target, or on usable objects within range |
| X-ray / ghost | Keep something important trackable behind walls | Outline-through or tinted hatch fill | Only for a few things that matter now: the held/interacted object, the objective, teammates |
| State feedback | Hit, damage, pick-up, cooldown ready | Fresnel rim flash, colour pulse | For a moment, on the affected object |
- Give each layer its own visual language (colour, width, animation) so they never read as each other: art lines are dark and quiet, gameplay highlights are bright and few.
- Scarcity is the point. A ghost outline on everything is noise; on the objective or the carried item it's guidance. Budget how many gameplay outlines can be on screen.
- Line art is an art direction choice, not a feature. It sets the tone (comic, anime, woodcut, blueprint) and interacts with shading, texture detail and camera distance: lines that look great close up can turn into black noise on distant, dense geometry, so scale or fade line weight with distance.
- Don't rely on colour alone for gameplay meaning (colour-blind players): vary width, pattern (the ghost hatch) or animation too.
- Fade in/out instead of popping, and hide gameplay outlines in cutscenes/photo mode.
Baseline
- Toon style: jump flood silhouette (anti-aliased, constant width, a slight ink edge or wobble) plus depth/normal/ID edge detection for thinner, softer inner lines. It works on any mesh with no normal baking, and it's cheap because the flood only runs a few passes.
- Gameplay highlight: jump flood + the two-channel mask for occluded parts (hide / outline / ghost). Only while something is highlighted, so cost is near zero otherwise.
- Whole-scene ink look: depth (inverse-depth Laplacian) + normal + ID edge detection, with hand-authored IDs where art needs control.
- Cheaper fallbacks: inverted hull on low-end targets or when you need per-vertex painted width; mask dilation for 1–2 px editor-style selection.
Related
- Fog — outlines usually composite after fog so they stay readable.
References
- Ben Golus — The Quest for Very Wide Outlines (jump flood vs. alternatives).
- Junya Motomura — GuiltyGear Xrd's Art Style: The X Factor Between 2D and 3D (GDC 2015).
- Rong & Tan — Jump Flooding in GPU with Applications to Voronoi Diagram and Distance Transform (2006).













