Documentation

// Bordercraft - Selection Outline for URP · v1.0.0 · Unity 6 / URP 17+

How It Works

Bordercraft hooks into URP's RenderGraph pipeline and draws a screen-space outline — a soft blurred glow, or a cheap hard edge line — around whichever GameObjects are currently selected. Selection is driven by flipping a reserved bit on the object's own renderers, so there's no scene setup and no per-project Layer configuration.

The outline only runs for a channel while at least one GameObject is currently selected on it. A channel with nothing selected costs nothing. This is the one thing most screen-space outline effects get wrong — they run their full pass every frame the moment the Renderer Feature is enabled, whether or not anything is actually outlined.

01
Scene renders normally

Geometry, skybox, and transparents render through URP as usual.

02
A GameObject is selected

Outliner.SetSelected(go, true) flips a reserved bit on every renderer under that GameObject.

03
Silhouette pass

The Outline Renderer Feature draws only the marked renderers into a coverage mask, touching only their own pixels. Every pass after this one is scissored to their on-screen bounding rect — not full-screen.

04
Composite pass

Soft style blurs and subtracts to extract a ring; Hard style skips straight to a cheap two-ring edge tap. The result is tinted and blended onto the camera image.

Requirements

RequirementVersion
Unity6000.0 or later
Universal Render Pipeline17.0.0 or later
RenderGraphEnabled in Project Settings → Graphics → URP Global Settings
Render path3D Universal Renderer — URP 2D Renderer not tested, not officially supported
PlatformsPC, Mobile — VR/XR not tested, not officially supported

Quick Start

01Open the Setup Wizard via Tools → Bordercraft → Setup Wizard. It also opens automatically on first import.
02Tick the URP Renderer assets you want to add the feature to, then click Add Outline Feature. A default profile is created and assigned to Channel 0 automatically.
03From code: Outliner.SetSelected(myGameObject, true); — no scene setup, no pre-placed component required.
04Tune the look by editing the OutlineProfile asset assigned to Channel 0 — style, color, radius, wave, etc. take effect immediately.

You can also create profiles directly via Assets → Create → Bordercraft → Outline Profile, or by clicking + Add Channel on the Outline Renderer Feature's Inspector to wire up a second channel with its own new profile in one click.

Demo Scene

Samples/Demo.unity walks through every feature with an on-screen button panel. The Samples folder is optional — nothing else in the package depends on it, so it is safe to delete.

01Run the Setup Wizard if you haven't already, so the Outline Renderer Feature is on your URP Renderer.
02On the Outline Renderer Feature, put Demo Channel 0–3 (in Samples/Profiles) into Channel 0–3. The buttons edit these assets directly, so a channel using any other profile won't change.
03Open Samples/Demo.unity and press Play.
HDR glow: the Enemy button uses an HDR color, which only glows when HDR is on in your URP Asset and Post Processing is on for the camera. The scene already includes a Global Volume with Bloom, and the panel warns about either setting if it's missing.

The buttons write into the demo profile assets, so the last look you picked stays in those assets after you exit Play. Press Reset to return to the default Soft look.

Channels

The Renderer Feature's Inspector shows one row per configured channel, Channel 0 onward, plus a + Add Channel button (up to 4 total). By default only Channel 0 is filled in, so Outliner.SetSelected(go, true) — which defaults to channel 0 — is all most projects need.

To show a second, independently-styled outline at the same time (e.g. a different color for enemies vs interactables), click + Add Channel, then select those objects with Outliner.SetSelected(go, true, 1). Each channel is fully independent — its own profile, its own zero-cost-when-idle gating, its own scissor rect — so an empty or unused slot costs nothing even while others are active.

Live status: in Play mode, each filled-in row shows a status dot (green = at least one object currently selected on that channel, grey = idle) with the active count next to it.
Missing profile warning: if an object is selected on a channel with no profile assigned, a warning is logged once (not every frame) naming the channel, instead of silently rendering nothing.

Outline Styles

StyleCostBest For
Soft (blurred glow)●●○○○A glowing halo — the default, reads well over any background
Hard (cheap edge line)○○○○A thin, crisp selection outline at the lowest possible cost

Soft grows the silhouette (Dilate, only when Gap or Wave is non-zero), blurs it (separable Gaussian, 2 passes), then subtracts the original from the blurred result to get a ring. Hard skips the blur entirely and samples two rings of the raw silhouette per pixel (near/far taps) instead — cheaper, but the line can't use the same wave-driven thickness variation trick as Soft.

Additive vs Opaque

OutlineProfile.opaque controls how the outline's color blends onto the scene.

opaqueBlendResult
Off (default)AdditiveThe outline color is added as light on top of whatever is behind it — always shows the background through it, however solid the color looks. Supports HDR bloom.
OnOpaque (premultiplied alpha)The outline replaces the background proportionally to Color alpha — fully opaque at full alpha, like a solid painted line.
These are not the same operation scaled differently. Raising Color alpha on Additive can never reproduce what Opaque looks like, except by coincidence over a pure black background. Over a bright or colorful background, Additive keeps the full background brightness and adds light on top of it; Opaque instead blends toward the outline color, replacing background brightness rather than adding to it, and never oversaturates. Pick Additive for a neon/glow look that should read even over bright scenes; pick Opaque for a flat, solid-color line.

Visibility & Occlusion

OutlineProfile.visibility controls how the outline resolves against the depth of the rest of the scene. It is decided per pixel, not per object — a half-covered target is outlined on the covered half only.

visibilityResult
Always (default)The outline shows through anything in front of the target — an X-ray highlight. The cheapest of the three: no depth buffer is bound at all.
Where VisibleThe outline is occluded normally, like any other object in the scene.
Where HiddenThe outline appears only where something else is covering the target — it fades in as the target moves behind an occluder and vanishes once it is back in the clear.

Where Hidden is the "where did my character go" effect — the classic outline that tracks a character walking behind a wall. It needs no bookkeeping on your side to switch on and off: an unoccluded target simply produces an empty mask, so nothing is drawn.

Self-overlap does not trigger it. A plant whose leaves cover each other, or a character holding an arm in front of its torso, stays clean — the mask is built by filling the whole silhouette and then erasing wherever the target is the frontmost surface on screen, so only geometry belonging to something else can leave a mark. One caveat to expect instead: the line traces the boundary of the hidden region, so it also runs along the occluder's own edge where that edge cuts across the target.

Cost. Both Where modes bind the camera's real depth buffer, which forces the silhouette buffer to full camera resolution with matching MSAA; Always skips that entirely. Where Hidden additionally draws the target's renderers a second time (the erase) inside the same render pass — no extra pass and no extra render target. Note that none of this makes a channel free when nothing happens to be occluded: zero-cost-when-idle is about having no active target, and an active-but-unoccluded target still runs the pass chain and produces an empty result.

OutlineProfile

An OutlineProfile is a serializable asset describing an outline's look — style, color, radius, wave, etc. It contains no rendering plumbing; that lives entirely in the Renderer Feature.

Ways to create a profile: Assets → Create → Bordercraft → Outline Profile, click + Add Channel on the Outline Renderer Feature's Inspector, or let the Setup Wizard create and assign one automatically.

Parameters

PropertyTypeRangeDescription
styleOutlineStyleSoft or Hard — see Outline Styles above
colorColor (HDR)Outline tint; alpha controls strength (Additive) or opacity (Opaque) — there's no separate intensity knob
opaqueboolAdditive glow (off) or solid opaque line (on) — see Additive vs Opaque above
radiusint1–32Soft: blur kernel radius in texels. Hard: edge tap distance in texels. Calibrated at 1080p, auto-scales at other resolutions
gapint0–32Pushes the outline out from the edge by this many texels instead of sitting flush against it (also 1080p-calibrated)
waveAmplitudefloat0–16Animates the outline's distance from the edge in a wave traveling around the object's screen-space center. 0 = static outline. Applies as set while the object is at least half the screen tall and shrinks with it below that, so a distant object's wave keeps its shape instead of turning into spikes
waveFrequencyfloat0–16How many wave bumps fit around the object's perimeter. 0 drops the position term entirely and the whole outline breathes in and out together instead. Whole numbers tile the perimeter exactly; a fractional value leaves a seam where the wave wraps — see Known Limitations
waveSpeedfloatHow fast the wave travels around the perimeter, in radians per second (about 6.28 is one full cycle per second). Safe to change at runtime — the wave speeds up or slows down smoothly
waveUpdateRatefloat0–30Quantizes the wave's motion into this many discrete steps per second instead of flowing continuously, for a stop-motion look. 0 = smooth
thicknessVariationfloat0–16Hard style only. A second, phase-shifted perturbation on the outer edge only, so the line's thickness varies unevenly along the outline instead of staying constant. Shrinks with distant objects the same way as waveAmplitude. No effect in Soft style
visibilityOutlineVisibilityWhich parts of the target get outlined — Always, WhereVisible or WhereHidden. See Visibility & Occlusion above

OutlineTarget

OutlineTarget marks a GameObject (and its child renderers) to be drawn with the outline. Most projects never touch this directly — Outliner.SetSelected adds and toggles it automatically. For objects you want pre-wired in the Editor instead, add it directly and just toggle its enabled checkbox — this works both in Play mode and in the Editor outside Play mode.

FieldTypeDescription
channelint 0–3Which channel this target shows on
renderersRenderer[]Renderers to outline. Leave empty to auto-collect from this GameObject and its children on enable

If any of those renderers is a Skinned Mesh Renderer with Update When Offscreen off, the Inspector shows a warning with a button that turns it on. See Troubleshooting for why it matters.

Call outlineTarget.Refresh() after adding or removing renderers on the object at runtime, so the outline picks up the change instead of continuing to use a stale cached list.

Runtime API

SelectionManager.cs
using Bordercraft;

// Select on channel 0 (the default most projects use):
Outliner.SetSelected(myGameObject, true);
Outliner.SetSelected(myGameObject, false);

// Select on a specific channel (0-3), routing to a different profile/look:
Outliner.SetSelected(myGameObject, true, 1);

Calling SetSelected is safe on any GameObject, including one that was never previously selected — no scene setup or pre-placed component required.

Common Patterns

Swap channels for different object types

SelectionManager.cs
void Highlight(GameObject go, bool isEnemy)
{
    Outliner.SetSelected(go, true, isEnemy ? 1 : 0);
}

Clear a previous selection before selecting a new one

Selection.cs
if (previousSelection != null)
    Outliner.SetSelected(previousSelection, false);

Outliner.SetSelected(newSelection, true);
previousSelection = newSelection;

Change a profile at runtime

Every profile field can be changed while the game runs and takes effect on the next frame. Two things to keep in mind:

A profile is a shared asset, not a per-object setting. Changing it changes every object on every channel and Renderer that uses that profile. To give objects different looks, put them on different channels.
In the Editor, edits made in Play mode stay in the asset after you exit Play. Player builds are unaffected, but put the original values back so your profile doesn't drift while you work — both examples below do.

Speed the outline up as the player gets close:

DangerPulse.cs
using Bordercraft;
using UnityEngine;

public class DangerPulse : MonoBehaviour
{
    [SerializeField] private OutlineProfile profile;   // the asset assigned to this object's channel
    [SerializeField] private Transform player;
    [SerializeField] private float dangerDistance = 5f;

    private float restSpeed;

    private void OnEnable() => restSpeed = profile.waveSpeed;

    private void Update()
    {
        float closeness = 1f - Mathf.Clamp01(Vector3.Distance(transform.position, player.position) / dangerDistance);
        profile.waveSpeed = Mathf.Lerp(restSpeed, restSpeed * 3f, closeness);
    }

    // Edits made in Play mode stay in the asset in the Editor, so put the value back.
    private void OnDisable() => profile.waveSpeed = restSpeed;
}

Switch a channel to a completely different look, and back:

AlertLook.cs
using Bordercraft;
using UnityEngine;

public class AlertLook : MonoBehaviour
{
    [SerializeField] private OutlineProfile profile;   // the asset assigned to this object's channel

    private OutlineProfile original;

    // An in-memory copy of the starting look, used to restore it.
    private void OnEnable() => original = Instantiate(profile);

    public void SetAlert(bool alert)
    {
        if (alert) ApplyAlertLook();
        else Restore();
    }

    // Write every field, so nothing from the previous look is left behind.
    private void ApplyAlertLook()
    {
        profile.style = OutlineStyle.Hard;
        profile.color = new Color(3f, 0.15f, 0.1f, 1f);   // above 1 glows when Bloom is on
        profile.opaque = false;
        profile.radius = 3;
        profile.gap = 2;
        profile.waveAmplitude = 5f;
        profile.waveFrequency = 10f;
        profile.waveSpeed = 10f;
        profile.waveUpdateRate = 4f;
        profile.thicknessVariation = 1f;
        profile.visibility = OutlineVisibility.Always;
    }

    // Copies every serialized field back from the saved copy.
    private void Restore() => JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(original), profile);

    private void OnDisable()
    {
        Restore();
        Destroy(original);
    }
}

Known Limitations

Scissor rect is one shared box per channel, not per object

Every currently-outlined object on a channel contributes to a single scissor rect spanning the union of all of them, not a separate shaded region per object. Fine for one-object-at-a-time selection; several outlined objects far apart on screen on the same channel will scissor a rect spanning all of them, growing the shaded (and therefore shaded draw cost) area accordingly.

Wave frequency and the wrap seam

The wave's phase is the angle around the object multiplied by Wave Frequency, and that angle wraps around somewhere along the outline — directly to the left of the object's screen-space centre. The two sides of the wrap only line up when Wave Frequency is a whole number, so a fractional value leaves a step there: a place where the wave jumps rather than flows.

It is worst at a half-step (0.5, 4.5) where the two sides are in full opposition, and it scales with Wave Amplitude — easy to miss at a small amplitude, and a visible notch cut out of the outline at a large one. 0, 1, 2, 3… are all seam-free, 0 included. The default of 4 is deliberately a whole number for this reason.

Wave is designed for one outlined object at a time per channel

The wave's phase is the angle around a single screen-space centre, and that centre is the middle of the same union rect the scissor uses — so with two objects far apart on one channel it lands in the empty space between them rather than inside either. Seen from there each object spans only a narrow range of angles, so the sine barely changes across it and the wave stops travelling around the perimeter: the whole outline pulses in and out together instead, with the two objects at an arbitrary relative phase.

It still animates and still looks deliberate, so this is a degraded effect rather than a broken one — but it is not the effect the parameter describes. A channel outlining one object at a time, which is the usual case and what the four channels exist to make easy, is unaffected. Leave Wave Amplitude at 0 on a channel that routinely holds several scattered targets.

The wave shrinks by one amount per channel

How much the wave shrinks for a distant object is measured from the largest outlined object on the channel. Several small, distant objects shrink correctly however far apart they are, but a distant object sharing a channel with a close one keeps the close one's full wave. Give distant objects their own channel if that matters.

Wave animation only advances in the Editor while the view repaints

Outside Play mode Unity only redraws a view when something happens in it, so a profile with Wave Amplitude set moves while you move the mouse over the Game view and holds still when you stop. In Play mode it animates continuously.

2D Renderer not supported

The render pass assumes the standard 3D forward/deferred pipeline (mesh silhouette draws, camera depth access for occlusion mode) and has not been tested against URP's 2D Renderer.

Rendering Layer Usage

This package permanently reserves the top 4 bits (28-31) of every GameObject's Rendering Layer Mask — not however many of the 4 channels you actually configure, all 4 are reserved the moment the package is present in the project, since they're a fixed constant in code rather than something that scales with use.

Check before importing: they're taken from the top of the mask specifically because URP projects typically only use bits 0-7 for Light Layers, so the odds of a collision are low — but if your project already uses custom Rendering Layers up near bits 28-31 for something else (lighting, decals, or your own filtering), check Project Settings → Tags and Layers → Rendering Layers before importing, or those layers will conflict with this package's.

Troubleshooting

Nothing shows when I call Outliner.SetSelected

Confirm the Outline Renderer Feature is added and enabled on the active URP Renderer asset (use the Setup Wizard to check — Tools → Bordercraft → Setup Wizard).
Confirm a Profile is assigned to the channel you're selecting on — check the Console for a "no Profile is assigned to that channel" warning.
If using a Wave setting outside Play mode, it only moves while the view repaints (see Known Limitations).

The outline is see-through even with a solid color

That's Additive mode (the default) doing exactly what it's designed to do — it adds color as light on top of the background rather than replacing it. Turn on Opaque on the profile for a solid line. See Additive vs Opaque above.

Part of an animated character's outline is missing

The outline is only drawn inside the character's bounds, the box Unity also uses to decide whether the character is on screen at all. A Skinned Mesh Renderer's bounds don't follow the animation by default, so a pose that leans or reaches outside them loses its outline there. Characters imported from VRoid (VRM) often have bounds much smaller than their poses.
Select the OutlineTarget and press Turn On Update When Offscreen in its Inspector, or tick Update When Offscreen on each Skinned Mesh Renderer yourself. This costs a little CPU per character each frame.
If you'd rather not pay that, set the Skinned Mesh Renderer's Bounds large enough to cover every pose instead.

Setup Wizard shows no renderers

Open it manually via Tools → Bordercraft → Setup Wizard.
If no renderers appear, create a URP Universal Renderer asset first: Assets → Create → Rendering → URP Universal Renderer.

Outline works in the Editor but is missing in a Player Build

This package's Materials are created purely at runtime, so no .mat asset in your project directly references these shaders. Unity's build pipeline strips any shader nothing references, which would otherwise remove them from the build even though everything works fine in the Editor.
The bundled Runtime/Resources/ folder contains one dummy Material per shader specifically to prevent this — anything inside a Resources folder is always included in a build. Don't delete or move these Materials out of Resources/.
If you still see this after confirming the Resources/ folder is intact, do a full clean Player build (delete Library/ShaderCache or the whole Library folder) so shaders recompile from scratch, since a stale build can reuse an older stripped shader binary.