← Keyline on 4ssets Quick Start Manual Live demo ▶

Keyline

Stylised inverted-hull outlines for URP and HDRP — with an X-Ray pass, interior fills, merge groups and thirteen drop-in components for the things outlines are usually bought for.

1 · Overview

Keyline draws an outline by building a second copy of the mesh, pushing it outward and drawing it behind the original. Everything in this manual follows from that one sentence.

It is a per-object effect. A hard-edged outline needs no fullscreen pass, no depth-normals prepass and nothing on a renderer feature list: an object has an outline because it has the component, and objects without it are not touched. A soft 3D rim is optional — one pipeline pass, added with a click, see 4.4. That is the trade against a post-process edge detector, which sees the whole frame at once and gives every edge in it the same weight. If you want one uniform line across the entire image, a post-process is the right tool. If you want this enemy outlined in orange through a wall while that pickup pulses green, this is.

1.1 What is in the box

Two properties are worth stating before anything else, because they are what most outline assets ask you to give up.

Your material is never touched. Keyline does not swap the shader on your renderer, does not require a specific one, and does not add a pass to it. The outline is drawn from a separate hull with its own material, so the source object keeps whatever it had — Lit, Shader Graph, a third-party toon shader, an asset-store character with a stack of custom passes. Nothing has to be re-authored to be outlinable, and removing the component leaves the object exactly as it was.

It adapts rather than demands. The same component covers a static prop, a skinned character, a flat card and a texture-cut leaf; it picks the hull it can build from the mesh in front of it. Pipeline, colour space, shadow settings and quality level are read, not dictated — there is no renderer feature required for a hard edge, no prepass to enable and no project setting the package insists on. Soft 3D rims optionally add one pass — 4.4.

1.2 Requirements

Unity6000.3 or newer
PipelinesURP 17.x and HDRP 17.x — one package, both supported
Built-in RPnot supported: the shader carries a SubShader per SRP and picks by tag
Tested onWindows (URP and HDRP), Android (Vulkan and OpenGLES3), WebGL
UntestediOS — not run, and this table says what was measured rather than what is expected
VRoutlines are stereo-aware and untested; Soft Edge is not. Its shaders address a single eye, so under single-pass stereo — instanced or multiview — the pass switches itself off and says so once in the console. Outlines still render, without the soft edge. Multi-pass stereo draws one eye at a time into ordinary targets and is unaffected
Both pipelines live in the same package and the same shader. Which SubShader compiles is decided by the pipeline tag, and which of the two the editor tooling assumes is decided by a define the package refreshes on import.

2 · Install

  1. Import the package. Everything lands in Assets/4ssets/Keyline/.
  2. Wait for the compile. The package looks at which pipeline packages are installed and writes the matching defines itself — if that ever goes stale, force it with Tools ▸ Keyline ▸ Refresh Pipeline Defines.
  3. Open Demo/Demo.unity and press Play.

Nothing else is required for a hard-edged outline. There is no renderer feature to add, no volume profile to author and no layer to reserve. A soft 3D rim is optional: one click adds a pipeline pass, and without that click Edge Softness does nothing — see 4.4.

The demo scene retargets its own materials. It ships authored against one pipeline's Lit shader; opened under the other, that shader does not exist and Unity would substitute the magenta error shader. The package notices and fixes it on first load — only the demo's own materials, and only when their shader is actually missing. If it ever has not, Tools ▸ Keyline ▸ Fix Demo Materials For This Pipeline runs it by hand. The outline shaders are never involved: they carry a SubShader for each pipeline and pick by tag.
The demo scene, X-Ray chapter
The demo, ten chapters of it, is the fastest documentation in the package — this is the X-Ray chapter with the prop halfway behind the wall.

2.1 Package layout

FolderHoldsNeeded at runtime
Runtime/ the component, the profile asset, the merge group, the recipes, the default materials yes
Shaders/ the outline shader, the Mobile Solid variant, the generated pipeline header yes
Editor/ inspectors, the bake tool, the variant stripper, the build hook that carries the project settings into a player no — stripped from builds by the assembly definition
Patterns/ 84 textures for the Pattern style only if you use them
Demo/ the demo scene, its scripts, models, character and cutout textures no

Deleting the demo is safe and expected. It is the largest folder in the package and nothing in Runtime/ references it. Delete Demo/ whole rather than in parts: it carries a link.xml and a WebGL plugin that belong to it and to nothing else.

The link.xml is worth knowing about while you keep the folder. Unity merges every one it finds under Assets/ into the managed stripping step of every build, and this one names eight physics types — the colliders and rigidbodies the demo adds from code, which the stripper cannot see and would otherwise remove, leaving the demo broken in a player build and working in the editor. It preserves those eight and nothing else, so the cost to a project that ships no physics is a handful of retained engine classes. Deleting Demo/ takes it with them.

The same applies to Patterns/ if you never use the Pattern style, and to the example art in general. See Licence and credits: it is public domain, so keeping, changing or deleting it is entirely yours to decide.

3 · Concepts

3.1 The inverted hull

The component builds a hidden copy of every mesh under it — a mirror — pushes its vertices outward and draws it with front faces culled, behind the original. The copy is bigger, so it shows past the edges of the real object and nowhere else. That ring is the outline.

Everything else in this manual is a consequence of the direction those vertices are pushed:

Width is authored in one of three units. World is metres, and keeps the outline in proportion to the object, so it thins with distance. Screen is pixels, and holds the same count at any distance — what a marker on a far objective needs, and what turns a crowd of distant objects into a solid mass. Pixels is a whole number of the sprite's own texels, and exists because an edge aligned to a texel grid cannot honour anything else; a mesh has no texel grid and ignores it.

3.2 The passes

One component draws up to six things, each with its own switch and its own profile asset — four that every outline has, and two Cutout Overlays that only a mesh offers:

PassDrawsTypical use
Main the rim, where the object is visible the ordinary outline
X-Ray the rim, only where the object is behind something an objective through a wall; a teammate through terrain
Fill the object's own surface, not a hull around it damage flashes, gauges, dissolves
X-Ray Fill the surface, only where the object is hidden a solid silhouette through geometry
Main Overlay a second draw of Main, with its own stencil pair a hull silhouette and an alpha rim on one object. Mesh only, experimental — see 4.1
X-Ray Overlay the same second draw, through walls as above, for the hidden half. Mesh only, experimental

The passes are independent in both directions: a solid rim around a perforated fill is a valid combination, and so is a thin grey Main rim with a bright orange X-Ray one. Each pass reads a complete profile, so "its own style" means its own everything.

The four passes, one per quadrant
One object, one pass at a time. Note that the two X-Ray quadrants only show anything where the wall is in front of the prop.
Rim only against rim plus fill
Left: the rim on its own. Right: the same object with the interior fill switched on. The fill is the surface, which is why it has no thickness to speak of.
A fill has no width. It is the surface itself, drawn at width 0 — the controller forces that, so the Width field on a fill profile is ignored. Colour, opacity and the style are what shape it.

3.3 Profiles are shared assets

A profile is a ScriptableObject. Assign one to a hundred enemies and there is one object in memory, which is exactly what you want for memory and for the SRP Batcher — and exactly what you do not want the moment gameplay writes to it. settings.color = Color.red on one enemy turns all hundred red, and in the editor the asset stays modified after play mode ends, because a ScriptableObject edited at runtime is the same object that sits on disk.

Instance Override is the answer and it is on by default. With it on, the first write forks the slot: the controller clones the asset, points the slot at the clone and writes there. Reads before and after return the same values, so nothing observable changes except that the shared asset is now safe. Forking is lazy — an object that only ever reads keeps sharing.

The rule that follows: go through the component, not the profile. outline.Color = Color.red forks; outline.Settings.color = Color.red does not, because at that point the profile is a plain object and nobody is watching. See 13.2.

3.4 The stencil budget

Outlines have to know where they are allowed to draw, and the answer is written in the stencil buffer. The pool hands out 126 unique pairs for a scene — enough for 126 objects whose outlines are independent of each other at the same time.

Past that, further objects fall back to a shared pair, and outlines that share a pair stop fencing each other off: where they overlap, the result is decided by whichever drew last. On a crowd this reads as flicker.

An object with a Main Overlay - Cutout Outline or an X-Ray Overlay - Cutout Outline assigned spends a second pair for that overlay. The hull still shares whatever silhouette it belongs to; the overlay keeps its own pair so Single Layer on the overlay profile can fence that rim without sharing the hull's stamp.

Two things spend from that budget, and both give something back:

So the practical limit is not "126 outlined objects" but "126 things that need to be told apart from each other". A forest of shrubs that always overlap is one of them.

Sharing the stencil with your own effects

Keyline writes the stencil buffer of the camera it draws into, and so may whatever else you have there — a Render Objects feature with a stencil override, a decal system, a custom pass of your own. Two effects writing the same bits do not fail loudly; they take turns, and each looks intermittently wrong.

What Keyline uses:

So an effect of yours that writes a fixed stencil value in the same frame will be overwritten by an outline drawn after it, and will overwrite one drawn before it. There is no bit to hide in. The ways out, in order of how well they work:

This is not specific to Keyline: eight bits shared between two systems that both want exclusive claims is the general problem. It is written down here because the symptom — an outline that is right until some other effect happens to draw — reads as a defect rather than as a budget.

3.5 X-Ray see-through layers

The X-Ray passes draw where an object is hidden. Out of the box "hidden" has one meaning for the whole scene — behind anything that wrote depth — and that is often too blunt. An enemy should show through the foliage in front of it and stay hidden behind the wall beside it, and those are the same answer to the depth buffer.

X-Ray Sees Through, in the Setup block of both outline components, narrows it per object. It is an ordinary LayerMask over GameObject layers:

The layer being read is the occluder's, not the outlined object's. Put the things you want to see through on a layer of their own, and list that layer on the outlines that may see through it.

Two objects behind the same wall can disagree, which is the point of putting the value on the component rather than in a profile or a scene-wide setting. Nothing anywhere else narrows, widens or overrides it: what the component says is what happens.

Generated child outlines inherit it — a figure is seen through the same things all over. A child carrying its own Keyline component keeps its own value, which is how one part of a model gets a different answer. Members of a merge group need no special rule: they share a silhouette and each draws by its own mask, so a pixel appears when any member both covers it and is allowed through.

It can be changed from script while the game runs, like any other Keyline parameter:

outline.XRaySeeThroughLayers = LayerMask.GetMask("Foliage", "Glass");

Occluders you can see through

Keyline decides what counts as an occluder by the render queue: opaque geometry writes depth, so the buffer this mask is read from agrees with the depth buffer it stands in for. A transparent material with ZWrite switched on is the one case where that rule is wrong. It does hide things — the ordinary X-Ray appears behind it exactly as it would behind a wall — but it sits in the transparent queue, so no mask can name it. Glass, a shop window, a water surface.

Transparent Occluder Layers, under X-Ray Layer Filtering in Project Settings ▸ Keyline, is where you name the layers those live on. It is empty out of the box and empty is a complete answer — everything on this page works without it, and almost every project leaves it alone.

Two things are worth knowing before you fill it in. Geometry on a named layer is collected whether or not it writes depth, so a pane that hides nothing will still block an X-Ray that is not allowed through its layer — keep the list to the layers that do the hiding. And the layer has to be one no outlined object is on, which is what putting occluders on a layer of their own already means.

The setting is stored where the render pipeline can read it in a build — on the renderer feature under URP, with Keyline's HDRP pass switches under HDRP — so the page shows one field either way and there is nothing to keep in step by hand.

What it costs

Nothing, until a mask is actually narrowed. With every outline seeing through everything there is nothing to test: the shader branch is not compiled in, no buffer is allocated and no extra geometry is drawn.

Once something does narrow, Keyline fills one small screen-sized buffer per camera, and — this is the part worth knowing — the cost does not grow with the number of different masks in the scene. Layers every narrowed outline sees through can veto nothing and are skipped entirely; layers no outline sees through all block equally and share one channel. Only layers your outlines actually disagree about need a channel of their own, and a project where every outline uses the same mask needs none. Giving fifty objects fifty different masks costs the same pass as giving one object one.

Three layers may be disputed at once. If more are, the extras fold in with the layers that block everybody and the inspector says which — so the failure is always less X-Ray than you asked for, never an outline showing through something it should not.

Turning it off

The feature needs Keyline's occluder pass in the render pipeline, and it is off until you ask for it — Project Settings ▸ Keyline ▸ Rendering Passes ▸ X-Ray Layer Filtering puts it there or takes it away, see 16.4. Keyline never adds a pass to your renderer unasked. Narrow a mask while it is off and the component says so on the spot, with a button to switch it on: the moment you are watching for the setting to work is the moment to be told it cannot. With the pass absent every mask is ignored and X-Ray behaves exactly as it always has; no component is edited and no value is lost. A missing pass never produces a picture you cannot explain.

In 2D, this is a second axis

A sprite scene has two layer systems and they answer different questions. Sorting layers decide who is an occluder — that is 12.4, and it stays a scene-wide decision. GameObject layers, this mask, decide which of those occluders a given outline can be seen through.

So a sprite can be an occluder and still let one particular character's X-Ray through, which sorting layers cannot express at all: a sorting layer is one number shared by everything drawing against it.

4 · The component

Keyline Outline 3D is the only component you have to add — Keyline Outline 2D if the object is a sprite. Everything else in the package either sits on top of one of them or is optional.

4.1 Outlines

No card is on the page until you add it. Main Outline, the overlays, the interior fills and X-Ray all stay off the inspector until you add them with Add pass — a freshly added component shows Setup and Tools and nothing else. A pass with no profile still draws nothing. Each pass is a card: colour chip, On in the header, profile slot in the body.

On toggles a pass without clearing its slot. The × in a card's header hides the row and turns that pass off; the profile stays assigned, so Add pass brings the same asset back.

RowWhat it holds
Main Outlinethe rim profile (hull, unless that profile has Cutout on)
Main Overlay - Cutout Outlinesecond draw of Main, own stencil: hull (outer silhouette) or Cutout (alpha rim). Single Layer lives on this slot's profile.
Main Interior Fillthe fill drawn where the object is visible
X-Ray Outlinethe rim drawn where the object is hidden
X-Ray Overlay - Cutout Outlinesecond draw of X-Ray, own stencil: hull or Cutout through walls. Single Layer lives on this slot's profile.
X-Ray Interior Fillthe fill drawn where the object is hidden
Experimental. Cutout Overlay — Main Overlay - Cutout Outline and X-Ray Overlay - Cutout Outline — is a second draw of that pass so a hull silhouette and an alpha rim can share one object. The look is not frozen. Cutout on solid/volumetric meshes is experimental for the same reason: Cutout is meant for flat cards (fences, foliage, decals). On a volume the rim sits on the surface; padding that widens a card in-plane is skipped.
The Keyline Outline 3D inspector
Passes added with Add pass. A slot with no profile draws nothing, and its switch has nothing to switch.

Clip At Intersections — objects that pass through this one

A rim is kept off its own object by the stencil: a mask stamps the body's claim and the rim refuses to draw where that claim is. The mask stamps it only where the body is visible, which is all it needs to be until something passes through the object rather than in front of it.

On the line of an intersection the two surfaces share a depth. The covered part of the silhouette carries no claim, the depth test is a tie, and the rim wins it — so the outline draws a band around the intruder's cross-section, on a face that has no silhouette there. A pillar through a crate gets a ring of outline where it enters. Ordinary overlap never shows this; only intersection does.

Clip At Intersections, in the Main Outline card, claims the whole silhouette for the length of the outline's own draws and hands it back before anything else reads it, so the rim treats the covered part as its own body. It is on by default, including on objects authored before it existed.

It is on the component rather than on a profile because whether anything passes through an object is a fact about where that object stands, not about the look it is drawn with — two objects sharing one profile should not have to agree about it.

CostsTwo stencil-only draws per source renderer on that object.
Turn it off whennothing ever passes through the object and those draws matter — a crowd, or a platform counting every call.

Where it does not reach. An intruder that carries an outline of its own has already claimed those pixels for its own rim, and a stencil cell holds one value, so the band can survive on it. The switch covers the visible outline only: an X-Ray rim draws where the object is hidden, so the silhouette it would be clipped against is the one it exists to show. The Cutout overlay keeps its own stencil pair and is not clipped either.

Match Rim holds this on. A depth-cut soft edge — Edge Softness above 0 with Halo Occlusion on Match Rim — removes the same band by a different route: the glow over an intruder's cross-section is thrown away because the object there is not in front of the scene. It stops one step short. Along the line of the cut itself the two surfaces share a depth, the comparison has no answer either way, and a hairline of outline survives it; closing that is exactly what the claim does.

So on those profiles the clip is held on and the switch is replaced by a note saying which control is holding it. Your own value is not overwritten and comes back the moment the profile moves to Ignore Depth or Edge Softness 0.

4.2 Setup

In the order the card draws them, so the table can be read down the page beside the component.

FieldDefaultWhat it does
Enabled On Starton Whether the outline draws from the first frame. Off is the usual choice for anything a highlight component switches on later.
Instance Overrideon Runtime writes fork the profile into a copy this object owns, instead of editing the shared asset. Leave it on unless you specifically want one object's script to change every object sharing that profile. See 3.3.
Outline Materialempty Empty uses the package material for the current shader profile. Assign your own only if you have modified the shader — the controller drives dozens of properties on it.
Smooth Normalson Averages normals across hard edges when the hull is built, so a cube's corners do not tear the outline open — and works out, per vertex, how much further that averaged direction has to travel for the faces to still clear Width. Miter Limit caps the second part; turning this off loses both. Costs a one-time analysis when the outline is built.
Cutout Paddingauto How far a Cutout rim may reach past the mesh's own edge. Auto works it out from the profile's Width and the source bounds; a manual value is for artwork whose alpha sits well inside the geometry. Read only when a profile on this object has Cutout on — see 8.
Include Childrenon Outlines every renderer under this transform, not just the one on it. What makes a multi-part prop read as one object.
Ignore Children Listempty Exceptions to the above — a hitbox, a socket, a glow card you do not want traced.
Auto Rebuild In Play Modeoff Rebuilds when the renderers under this object change while the game is running — a weapon swapped, a limb detached. Off by default: in a build this is a standing cost paid to catch a case most projects never have. The Editor has a separate, project-wide switch for the same thing; the two never act at the same moment. See 16.4.
X-Ray Sees Througheverything Which occluders this object's X-Ray may be seen through. See 3.5.
Merge Child Outlineson Children with their own Keyline component join this object's silhouette instead of drawing separately. See 9.
Merge When Overlappingon Two outlines whose rims meet draw as one silhouette. See 9.
Layer Maskeverything Which renderers under this object are eligible at all.
Preview In Edit Modeon Builds the mirrors in the editor so the outline is visible without entering play mode.

4.3 Tools

4.4 Soft Edge (optional)

A 3D rim is hard by default. That is the product: hang the component on an object and you are done. Softness is a paid extra in setup, not a new default.

A hard rim beside a soft one
The same rim and the same width. Left: Edge Softness 0, the default. Right: about 4.

Edge Softness on a rim profile is a world-space halo, 0 to 8. One tick is about a centimetre. Zero is the old picture. The fade stays around the object as the camera moves — it does not spread in screen pixels. The value is read by Main Outline, X-Ray Outline, and their Overlays (Halo and Double extra layers travel with the rim). Interior Fill and X-Ray Interior never soften — they stay on the camera path.

A rim drawn from a distance field does not use this at all — neither a sprite nor a rim with Cutout on. Those already carry a softness of their own, Rim Softness, and it is the better one: the field knows the distance to the edge, so the fade is resolved per pixel at the place the edge actually is, for the price of the fetch that placed it. This pass exists for the opposite case — a hard hull silhouette, which has no gradient anywhere in it to widen. The field is not offered both, and the slider is hidden rather than left to do nothing.

Softness above zero does nothing until the Soft Edge pass is in the pipeline. The profile tells you so beside the slider and offers a button to add it; the same switch project-wide is Project Settings ▸ Keyline ▸ Rendering Passes ▸ Soft Edge. The rim stays hard rather than failing silently.

When the pass is present and softness is above zero, those rim mirrors leave the camera's transparent queue and are drawn into an off-screen buffer, Kawase-blurred, then composited. The blur radius is converted from world centimetres at the object's depth, so the halo stays around the mesh instead of spreading as the camera pulls away. After the blur, the object's own mesh is punched out of that buffer (depth test against the camera copy) so the glow stays around the silhouette and does not paint the albedo. Objects in that buffer share its stencil and fence each other as they did on camera. Soft rims and hard rims (including every Interior Fill) do not share the camera stencil in the same frame — Single Layer on a soft object will not fence a hard neighbour, and the other way around.

Each profile uses its own world radius. Grouped objects share one isolation RT so their stencil still matches. One object's Edge Softness does not change another.

Glow Outside Only — keeping the soft X-Ray glow off the body

A hard rim is already fenced out of the object's own silhouette: the mask stamps the body's stencil claim and the rim tests against it. A blur has no stencil. Raise Edge Softness and the glow spreads inward as well as outward — over the very area an Interior Fill occupies, so the fill ends up with a glowing border painted on top of it.

Glow Outside Only sits in each rim's own card — Main Outline and X-Ray Outline — because the rim is what it changes. It answers one question for both: is the glow confined to outside the silhouette, or may it wash across the object?

The pass already removes the object's body from the blurred rim, so the glow does not land on its albedo — but only where the body wins the depth test. Hidden fragments are left glowing on purpose: that is the X-Ray look. From there the two families reach the same answer from opposite sides, which is why their defaults are opposite and both are what Keyline has always drawn.

RimDefaultOnOff
MainOn Today's look: the glow stops at the silhouette. Nothing is cut. The glow falls off inward over the object and over Main Interior Fill.
X-RayOff The whole silhouette is cut, hidden fragments included. Today's look: the glow stays on the hidden part of the body.

A Main rim only exists where its object is visible, so the ordinary cut already confines its glow and switching off means cutting nothing at all. An X-Ray rim exists where its object is hidden, which the ordinary cut spares on purpose, so confining it takes the whole silhouette.

Turning Main off gives a Main rim the look an X-Ray rim has by default — a glow lying over the object. The rim's own colour still never lands there; stencil fences that, and only the blur reaches inward. So it reads as a falloff from the edge rather than as a fill.

It is on the component rather than on a style profile because it describes this object's silhouette, not a look: two objects sharing a profile should not be forced to agree about their own bodies, and a merge group shares one owner across several profiles. Each row appears only where it can act — while that card's own profile has Edge Softness above 0. With a hard rim there is no blur to keep out and the row is simply not there.

This is not the same control as Halo Occlusion below, and the two solve neighbouring problems. Glow Outside Only is about the glow lying over this object; Halo Occlusion is about the glow lying over the scene. They can be used together.

Halo Occlusion — whether the glow obeys the rim's own depth rule

The rim itself is depth-tested: a Main rim never appears behind a wall, an X-Ray rim never appears in front of one. The blur is not. It is a full-screen filter with no notion of depth, so it spreads the rim's colour straight across the line the depth test just drew. Both families get an artefact out of it, and they are mirror images:

Halo Occlusion, next to Edge Softness, decides whether that is what you want.

Match Rim also holds Clip At Intersections on for the Main rim of anything wearing the profile. The two remove the same band and the cut leaves a hairline the clip closes — see the note in 4.1. The component's own switch is left as you set it and returns under Ignore Depth.

Upgrading from 1.1. A project with Edge Softness above 0 will look different after this update: the glow now stops where the rim does. Set the profile to Ignore Depth to get the old picture back — the option did not exist before, so there was no way to have chosen it.

It is one rule, not two features. The comparison is the same for both families and only its direction follows the pass, which is why the setting reads identically on either profile and needs no exception. An earlier version exempted X-Ray outright, on the reasoning that being visible through walls is what an X-Ray pass is for — true, and beside the point: honouring the rim's rule is exactly what keeps the X-Ray glow on the far side of the wall instead of spilling off it.

What it costs. A halo has no depth of its own once it has been blurred, so one has to be built: a screen-sized two-channel target, seeded from the object's own mesh and spread with the same filter footprint the blur uses. It is allocated only when something in view actually asks for it — which, Match Rim being the default, means as soon as any visible rim has Edge Softness above 0. A scene with no soft rim allocates nothing, and neither does one whose profiles are on Ignore Depth. Edge Softness itself starts at 0, so a project that never uses soft edges never meets this cost.

How far it reaches. The depth field is built from the object's mesh and spread as far as the blur reaches, so within that distance the test is exact. Further out — the band the rim's own width contributes, beyond anything the blur touched — there is no depth to test against, and the glow is left alone rather than guessed at. In practice that means a rim much wider than it is soft keeps an untested outer fringe: it may survive on a surface it would otherwise have been trimmed against. Raising Edge Softness, or narrowing Width, closes the gap.

Leaving it untested is deliberate. An earlier build filled that band with the object's centre depth instead, and the guess was wrong often enough to cut the fringe away — the outline appeared to eat its own edge at any softness too small to span the rim. Showing a little too much is a fault this feature is allowed to have; erasing what it was asked to trim is not.

4.5 Draw Config (queue and depth)

Queue, ZTest, ZWrite, Cull, depth bias and stencil Comp / Op / masks are hardcoded until you add a scene host. Without KeylineDrawConfig in the scene, every 3D outline uses the same factory numbers as the current working defaults — no ScriptableObject, no Resources asset.

The host is optional. Tools ▸ Keyline ▸ Add Draw Config creates a Keyline Draw Config object if the scene does not already have one, writes Assets/KeylineCustomDrawConfig.asset if that file is missing, and assigns it to Custom Draw Config. Running the item again does not spawn a second object or a second asset. There is no Create Asset menu for this type.

Reset All to factory on the assigned asset writes those working defaults back.

The inspector is three tabs:

These knobs can break hollow X-Ray. X-Ray Outline ZTest must stay Greater, and its stencil WriteMask must stay None. The overlay family must not overlap the hull’s queue block. The inspector warns; it does not block the edit. Stencil which bit an object owns is still allocated by the pool — that is not in this asset.

Soft Edge isolation (drawing X-Ray into a private RT) is not a Draw Config field. It stays a pipeline pass, see 4.4.

5 · The profile

A profile is an asset: Assets ▸ Create ▸ Keyline ▸ Style Profile, or the New button next to any empty slot. Beside it in the same menu sits Mobile Solid Style Profile — the same asset drawn by a lighter program, with no Mixed expand and no mesh Cutout, and that absence is where the saving comes from. See 16.

Sixteen profiles ship in Runtime/Profiles/: one per style, plus Profile_MobileSolid, the bundled Mobile Solid one. Tools ▸ Keyline ▸ Restore Bundled Style Profiles puts them back if they are edited by accident — rewriting the ones still there, and recreating any that have been deleted.

The inspector hides what the current style does not read. That is deliberate and it goes both ways: a control that does nothing is worse than a missing one, because it invites tuning and then teaches the wrong lesson.

The profile inspector on the Gradient style
The Gradient style with the Radial shape. The inspector shows what this style and this shape read, and hides the rest — Angle becomes Stretch Axis here, and the linear-only fields are gone.

5.1 Look

FieldWhat it does
Style Which of the fifteen looks this profile draws. See 6.
Color HDR. Its alpha is part of the look, not a master fade — that is Opacity.
Secondary Color Only for the styles that use two: Double, Gradient, Frost, Lava, Smoke, Crystal, Water, Fire. Under Gradient it is the start of the ramp and its alpha is the alpha at that end — a transparent start against an opaque main colour is what makes a gauge empty out instead of changing colour.
Width Thickness of the rim. Metres in World mode, pixels in Screen mode, and a whole number of source texels in Pixels mode. The field's label and units follow the mode, and in Pixels it becomes an integer with a step button either side.
Edge Softness World-space halo on a 3D rim (0–8). One tick is about a centimetre. Stays around the object as the camera moves — it does not spread in screen pixels. Zero is a hard edge. Main, X-Ray and their Overlays read it; Interior Fill ignores it; a rim drawn from a distance field does not show it at all — sprites and Cutout soften through Rim Softness instead. Needs the Soft Edge pass — 4.4. Each profile uses its own value.
Halo Occlusion Whether the halo obeys the same depth rule its hard rim obeys. Match Rim, the default, keeps the glow behind whatever hides the rim, so a soft Main outline stops at an occluder and a soft X-Ray outline is the mirror of that. IgnoreDepth lets the glow lie over the scene — the pre-1.2 look, and still the right one for a lamp or an aura that should wash over what is in front of it. Shown only while this profile's Edge Softness is above 0. Not the same control as Glow Outside Only, which is about the object's own body. See 4.4.
Width Mode World keeps the outline in proportion to the object, so it thins with distance and goes away with the thing it is on. ConstantScreen holds the same pixel width at any distance — right for a marker on a far objective, wrong for a crowd, which turns into a solid mass of outline. Pixels counts texels of the sprite's own texture and is for 2D pixel art — see 12.3.
Opacity Master fade for the whole pass, 0 to 1. This is the one to animate.
Single Layer Paint each pixel once where this pass's hull covers it twice. Each pass reads its own profile — X-Ray Outline and X-Ray Interior honour the flag the same way Main does. Turning X-Ray on does not fence Main Outline. Hidden for Halo, Frost and Double, whose overlap is the look. See 5.6.
Face Cull

Which faces draw this pass. Rim passes: Back is the classic inverted hull, Front keeps the near side, and Both is for single-sided sheets — cards, foliage, cloth — that must read from either side. On those Both also turns the extrusion toward the camera so the outline is not hidden behind the surface it belongs to. Which parts of a model are sheets is measured when the outline is built, so on a solid model Both finds nothing to turn — but it still does the other half of its job.

Both draws both walls of the hull. A hull has a near wall and a far wall, and Front and Back each keep one of them. Both keeps the pair, so a style with depth to it — Fire, Smoke, Crystal — reads as the volume it is rather than as a single surface. Depth writing is turned off while Both is on, because a hull that writes depth can only ever keep the nearer of its two walls.

Interior fills honour it too. Width is 0 on a fill, so the geometry is the model's own surface and nothing is extruded — the setting only picks which faces of it draw. Front, the default, is the near side, which is what a fill on a solid model wants. Both adds the far side and is what makes a Cutout fill visible on a single-sided card seen from behind. Back swaps to the far side alone, which on a solid model sits behind its own near surface and fails the depth test almost everywhere.

5.2 Style space

Styles that draw a pattern, a noise or a ramp need to know what that pattern is attached to. Style Space is that answer, and only those styles read it.

Ignore Object Stretch, directly under it and only while the space is Object, decides whether a non-uniform scale is allowed to stretch the pattern. Object space is the model's own local position, so scaling a transform (2, 1, 1) leaves that coordinate alone while the mesh becomes twice as wide — noise cells and flow axes then come out elliptical. Smoke and Fire show it most, because their flow axis turns the skew into a lean. On by default; turn it off for the pre-1.2 behaviour, where the style stretches along with the mesh.

A uniformly scaled object is untouched either way, and that is arithmetic rather than a promise: the correction is normalised so it comes out exactly 1 there. Only an object that was already being stretched can look different. The other three spaces do not measure in local units, so there is no stretch in them to remove and the row is not shown.

Upgrading from 1.1. Profiles authored earlier read this as on, because the setting is stored as an opt-out and an older asset has nothing stored. A non-uniformly scaled object using an Object-space style will therefore look different — that is the fix arriving, not a regression. If a particular object relied on the stretch, turn the box off for its profile.
SpaceThe pattern is anchored toReads as
ObjectSpacethe meshpainted on; it turns with the object
WorldSpacethe scenethe object moves through a fixed field
ScreenSpacethe screena filter over the image
UVSpacethe mesh's unwrapa texture on the surface — needs sane UVs

A new profile starts in Object Space whatever its style, and Reset puts it back there. That is the answer that needs no explaining: the effect sits on the object and travels with it. Anything else is a deliberate choice, and a deliberate choice belongs to the person making it rather than to a default. Profiles authored before Style Space existed keep the space they were saved with, which is the one case where reproducing the old look is the whole point.

Two styles offer three of the four. Pattern and Gradient both measure along a single axis from the object's pivot, which on an unrotated object is the same number in world space as in object space — a mode that duplicates its neighbour most of the time is worse than one fewer mode. So World is not offered for them, and a profile carrying it is moved back to Object.

5.3 Expand

FieldWhat it does
Expand Mode Normal pushes along the surface normal, Planar stays in the plane of the face, Mixed decides per face. The whole of section 7 is about choosing between them.
Miter Limit
default 1.45
How far a vertex on a hard edge may be pushed beyond the plain offset, so that edge keeps the same outline thickness a curved surface gets. This is what makes a box and a sphere match on one profile — see 7.1 for why they otherwise do not. The default clears the 1.41 a right-angled edge asks for, so square edges and cylinder rims reach the full Width, while a three-face corner is held a little short and eases in rather than spiking out. Raise it towards 2 for a perfectly square corner; higher only matters on acute spikes, where the exact correction runs away. 1 means no correction at all. Needs Smooth Normals, and applies under Normal and Mixed.
Expand Center Mode Where in-plane expansion radiates from: the mesh's own bounds, or a point you give it. On a model whose bounds centre sits off the shape — a hilt, a stalk — that origin is what makes one side of the outline heavier than the other.
Planar Width / Height Scale Stretch the in-plane expansion along the object's local X and Y. For sheets that should grow more one way than the other.
Sheet Solidify and its scales Gives detected sheets a thickness so they stop being infinitely thin. Covered in 7, where it can be shown rather than described.
Under Cutout this entire section is ignored: the hull is not extruded at all, and the rim is drawn by widening the texture's alpha instead. The inspector hides Expand when Cutout is on, for exactly that reason.

5.4 Pulse

A cheap animated breath, computed on the CPU once per frame and pushed as two numbers — no coroutine, no animator, no per-object script.

FieldWhat it does
PulseOn or off.
Pulse SpeedCycles per second, roughly.
Pulse AmountHow far it swings.

On a rim, Pulse drives the width and brightens the colour with it. On a fill it drives the colour only, because a fill has no width to swing.

5.5 Blend Mode

How the pass is composited over what is behind it — a rim, an interior fill, either of the X-Ray pair. It is a property of the profile, so each pass can be laid over the frame differently. In 1.0.0 the style decided this by itself and there was no field; Default is that behaviour kept intact, so every profile written before this existed looks the same as it did.

The mode matters most where there is something underneath for it to act on, which is why the picture below is a fill: an interior fill covers the model's own lit surface, and what a mode does to that surface is the whole of what separates the modes from one another.

ModeWhat it does
DefaultThe style decides. Neon, Halo, Electric and Rainbow add their colour to the frame; every other style blends with transparency.
AlphaOrdinary transparency, and the only mode where Opacity covers what is behind it. Pick this to make a light style solid.
AdditiveLight. Bright over dark; over a pale background it clips to white and the shape goes with it.
ScreenLight that cannot blow out — approaches white instead of hitting it, so a glow keeps its shape over bright scenery. Usually the safer of the two glows.
MultiplyInk. Darkens and tints rather than covering: a contour, a coloured shadow, a stamped line. Opacity reads as how far towards the colour to go.
SubtractThe colour taken out of the frame. A hole cut in the light.
Darken / LightenWhichever is darker or brighter, channel by channel. Lighten is the glow that stops climbing however many outlines overlap.
Opacity means different things in different modes. Under Alpha it covers. Under Additive and Screen the background always shows through and Opacity dims the colour instead. Under Multiply and Darken it reads as how far towards the colour to take the frame. A slider that behaves differently for reasons off-screen is worth knowing about before tuning against it — the inspector says so where it matters.
One model's interior fill in each blend mode
The same model, the same colour, one mode per cell — shown on the interior fill rather than on a rim. A mode decides what happens to what is already under the paint, and a fill is laid over the model's own lit surface, so there is something under it to see: watch the shading, the texture and the shadowed side survive or disappear from cell to cell. A rim two pixels wide has almost nothing behind it and so shows almost none of this.

5.6 Single Layer

A hull covers some pixels twice — wherever the shape folds back on itself, and at every seam of an object built from several meshes. At full opacity that is invisible. Drop Opacity to about a half and the doubled places show as bright patches, because two half-transparent layers are not one half-transparent layer.

Single Layer paints each pixel once instead. Once per unit: the whole object with all its renderers, or the whole merge group. Two separate objects still draw over each other — that is what merging is for.

It is 3D only. A sprite outline is cut from a distance field on a quad, and a quad does not cover any pixel twice, so there is nothing to fence; the 2D inspector says so if a profile carrying the flag is assigned there.

Not available on Halo, Frost or Double. Those styles are built from stacked hulls whose overlap is the falloff — fencing them would leave one layer standing. The field is hidden for them, and the check is repeated in the code, because a profile outlives the style it was set up under.

Doubled overlap against Single Layer
The same object at half opacity. Left: the seams and the fold show as brighter patches. Right: Single Layer on.

6 · Styles

Fifteen looks, one profile field. Switching the style in the inspector or in the demo brings that style's own defaults with it — colour, width and its own parameters — because a style is a specific set of numbers rather than just a branch in the shader.

Cost. Twelve of the fifteen are one hull and one draw call per object. Three are not, and it is worth knowing which before a crowd scene:

StyleHulls per objectWhy
Solid, Sketch, Pattern, Electric, Rainbow, Gradient, Neon, Lava, Smoke, Crystal, Water, Fire1one band, drawn once
Frost2a soft shell around the core
Double3 on a mesh, 1 on a spriteinner ring, outer ring, and a mask that keeps the gap empty — or all three cut from one distance
Halo2–8 on a mesh, 1 on a spriteone per layer — or the whole falloff in a single draw, cut from a distance field

All fifteen shots below are the same prop, the same camera and the same light. What differs is the style and its shipped defaults.

6.1 Solid

One hull, one draw call — the cheapest the asset gets, and the right default for anything that just needs to be picked out. Every other style is measured against this one.

The Solid style
The reference: an even contour of constant width, and nothing else happening.

6.2 Neon

An additive core that reads as light rather than paint. Glow pushes the colour past white; the shipped profile also switches Pulse on, because a neon sign that does not breathe looks like a decal.

Additive blending means the background shows through the bright parts — over a dark scene it glows, over a bright one it washes out. That is the trade of the style, not a fault of it.

The Neon style
Additive core. The brightness comes from Glow Intensity, not from the colour being lighter.

6.3 Halo

A falloff instead of a line: several concentric hulls, each slightly wider and slightly more transparent than the last. Layers is how many, and it is the only control the style has. The ramp between them is a straight line from the object out to nothing, and it is not adjustable — it had an outer alpha and a fade shape once, and both were taken away. The layers carry the differences between levels of the curve, so whatever those two were set to, the sum came back to almost the same value at the inner edge: two sliders that between them reached one thin band are worse than no sliders at all. Width and Layers say everything this style needs told.

On a mesh each layer is a draw call, which is why the count stops at 8 there. Eight layers on one hero object is fine; eight layers on forty objects is three hundred and twenty draws. A sprite cuts the same bands out of its distance field in one draw and goes to 32.
The Halo style
Five layers, linear falloff. What you are looking at is the gradient, not a thick line.

6.4 Sketch

Hand-drawn unevenness: the width is perturbed by noise, redrawn at a discrete rate rather than every frame, so it reads as a pen redrawing the outline instead of static jitter. Amount is how uneven, Speed is how often it is redrawn.

The Sketch style
The width varies along the contour. Speed controls the redraw rate, not the motion.

6.5 Pattern

A texture across the outline — dashes, dots, hazard stripes, whatever is in the 84 shipped PNGs or your own. Scale, Contrast, Threshold and Scroll shape it.

What the pattern is anchored to is Style Space: painted on the mesh, fixed on screen, or following the UVs. World is not offered here — measured from the pivot along one axis, it would repeat what Object already does.

The Pattern style
A pattern with obvious structure reads best. Scroll it and the outline becomes a marching ant.

6.6 Double

Two concentric rings with a gap between them. Inner Width, Gap and Outer Width are all in the profile's width units. The profile's shared Width is not read by this style and is hidden for it — these three are the whole of the ring.

On a mesh this is three draws per object: the two rings, plus a mask that keeps the gap genuinely empty rather than filled by whatever is behind. A hull knows only its own radius, so the core and the ring cannot be told apart within one.

On a sprite it is one draw. Both boundaries are thresholds on the same distance, and the gap is simply where neither ring is — nothing is drawn there, so nothing has to be masked out.

The Double style
Two rings and a real gap — the mask is what keeps the middle from filling in.

6.7 Electric

Sketch's noise driven harder and faster, with a floor under the width so the bolt cannot collapse to nothing between spikes. It shares Amount and Speed with Sketch, but the two do not mean the same thing here: Electric stacks three noise terms where Sketch uses one, so the shipped profile asks for less Amount — 0.5 against Sketch's 1.2 — and far more Speed, 2 against 0.35.

The Electric style
A frame at the peak of a spike. The floor under the width is why the line never breaks.

6.8 Rainbow

A hue sweep along the outline. Motion chooses whether the bands travel up the object or rotate around a centre; Scale is how many bands, Speed how fast — negative reverses it.

Saturation takes the ramp towards grey rather than towards white, so the bands stay readable as light and dark all the way down to zero. A desaturated Rainbow is a travelling shimmer that does not fight the scene's palette.

Rainbow is the one style that does not read Colour: every channel comes from the hue ramp, so the field is hidden for it rather than left there doing nothing. Opacity and Saturation work as expected.

The Rainbow style
Vertical motion. Circular puts the centre wherever the offsets say, which is what a shield wants.

6.9 Gradient

The largest style in the asset, and the one everything else is built on: the waves, the gauges and the reveal all drive a Gradient underneath.

Two colours with independent alphas, three shapes and a set of controls per shape — see 5 for the full field list. In short: Linear is a ramp in the object's own space; Radial and Planar measure from a point or a plane in absolute world space, which is what lets one front cross several objects in step.

The band is symmetric. With Band above zero the ramp becomes a travelling stripe, and the second colour is then what the object looks like everywhere the stripe is not — not a trailing colour. Give it alpha 0 unless you want the whole model tinted.
The Gradient style
Linear, one colour at each end. Drive Offset from code and the ramp becomes a level or a wave.

6.10 Frost

A soft, cold edge: a wide low-opacity shell around a core, with sparkle noise on top. Soft Falloff shapes the shell, Halo Width and Halo Opacity size it — the labels say halo because that is what the outer mirror reads as — and Sparkle is the noise. Band Fade is how large a patch of plain ice is left where the surface turns to face the camera; it is worth leaving alone, because the strata are keyed to a direction that stops meaning anything head-on, and at zero they spin into a cross there. Uses the secondary colour.

Two hulls per object — the core and the shell.

The Frost style
Shell plus sparkle. The second colour is doing half the work here.

6.11 Lava

A dark crust broken open on a moving core. Cracks is how much of the crust has split — at zero it is unbroken and the style is a dark rim, at one there is no crust left and the whole band is molten. Heat is the brightness inside the cracks over the outline's colour; past one it goes white-hot, which is what sells it as emitting rather than merely being orange.

Flow and Flow Direction move the crust, Crust Scale sizes it, and Turbulence decides how much the flow folds rather than slides. Pulse Rate and Pulse Depth breathe the heat. Uses the secondary colour for the crust.

The Lava style
Cracks at about half. The dark parts are the crust; the light comes from Heat, not from the colour being brighter.

6.12 Smoke

A billowing cloud rather than a line. Density is how much of the band is filled, Billow Scale sizes the puffs, Drift, Drift Direction and Drift Tilt carry them — the tilt leans the drift out of the style space's XY plane, which is the only plane a single angle can point in — and Turbulence is how much the drift curls instead of sliding.

Height is how far along the drift the cloud lasts, as a fraction of the shape — the same control Fire calls Height, and it reads the same way. At 0.3 the cloud clings to where it started and is gone a third of the way along; at 1 it just reaches the far edge; past 1 it leaves the shape still thick, which is what smoke coming off a thing rather than sitting on it looks like. It is subtracted from the cloud rather than clipping it, so the smoke ends by having too little substance left to show — which is what stops it reading as a painted shape with an edge.

Fray is the manner of that ending, once Height has decided where. Low keeps what survives opaque and gives it a defined edge, closer to steam under pressure; high stretches the last of it into wisps that fade out over a distance. Uses the secondary colour.

The Smoke style
Drifting up and fraying out. Nothing here has a boundary; the cloud simply runs out.

6.13 Crystal

A cut stone: the band is partitioned into facets, each one a flat plane catching the light on its own. Facet Scale is how large they are, Evenness how regular — low gives a rough rock, high a machine-cut gem. Cut Edges is the brightness of the lines between facets, and it is what makes the shape read as cut rather than as painted.

Glint is how much fire the stone has. Zero is a stone with none — not a dim one, none — and upward the highlight widens and brightens, so more facets catch the light at once. Low leaves one or two glinting while the rest stay dark, which is what reads as cut; high lights most of the stone, which reads as polished. Glint White is the ceiling on how far a lit facet washes out — at zero it keeps its own colour, which suits a coloured gem; at one it blows out like cut glass. Dispersion splits the colour between neighbouring facets, small amounts reading as a precious stone and large as a prism. Light Direction and Spin move the light; Facet Depth shades each facet by distance from its own centre, which trades flat panes for pebbles. Uses the secondary colour for the fringe the facets are split towards.

The Crystal style
Cut edges bright, a couple of facets catching the light, the rest dark. Dispersion is what makes neighbours disagree about the colour.

6.14 Water

A surface flowing around the object. Flow and Flow Direction carry it, Ripple Scale sizes the ripples, Turbulence folds the flow rather than sliding it.

Caustics is the bright lacing where the surface focuses light, and Sheen is the gloss along the crests where the surface turns towards the viewer. The two share one blend towards the crest colour, so together they reach it and stop rather than adding up into white. Uses the secondary colour.

The Water style
Caustics and sheen together. They are deliberately bounded — no setting of the two can blow the band out.

6.15 Fire

Tongues rising off the silhouette. Rise and Rise Direction are the speed and the way up. Height is how far the tongues reach as a fraction of the shape: at 0.3 the fire clings to the base and dies a third of the way along, at 1 it just reaches the far edge, and past 1 the tips run off the edge instead of dying inside it — which is what a fire that is over the thing rather than on it looks like.

Stretch is how many times longer a tongue is than it is wide. At 1 they are round — blobs of heat rather than flame — and raising it squashes the pattern along the rise direction, which is the whole difference between noise and fire. Tongue Scale, Rise Tilt, Turbulence, Heat and Flicker shape the rest — the tilt leans the climb out of the style space's XY plane, which is the only plane Direction alone can point in. Uses the secondary colour for the cooler tips.

The Fire style
Height at 1 and Stretch well above it. Drop Stretch to 1 and the same noise reads as bubbles.

7 · Hard meshes

This is the section the asset exists for. Everything else here can be reproduced with a short inverted-hull shader from a forum post; this cannot.

7.1 Why a normal-based hull fails

Pushing every vertex along its normal works while the mesh has volume. Where it does not — a leaf, a blade, a card, a thin prong — the normals on the two sides of the sheet point in opposite directions, so the two sides move apart. A flat leaf becomes a wedge. A fork's prongs, thin and close together, swell until they touch and the gaps between them close.

The three pairs below are the same prop, the same width and the same camera, with one switch between them.

Carrot: naive hull against Keyline
The carrot is one mesh of two kinds: a solid root and flat leaves. A single expand rule cannot serve both — push the leaves along their normals and they inflate into blades.
Fork: naive hull against Keyline
Thin prongs, close together. The naive hull closes the gaps and the fork becomes a paddle; four prongs and four gaps is the correct answer.
Leek: naive hull against Keyline
The same split as the carrot with the proportions reversed: here the flat part dominates, so the failure takes over the whole silhouette.

The quieter failure: hard edges come out thinner

The same mechanism has a second consequence, and this one shows up on models that are perfectly solid. A vertex where several faces meet has one normal, averaged between them. Push along it by Width and none of those faces actually travels Width — each moves out by Width × cos θ, where θ is the angle between the shared normal and that face.

On a smooth surface θ is nil and the outline is exactly as wide as asked. On a hard edge it is not:

Shapeθ at the silhouetteOutline you actually get
Sphere, capsule≈ 0°1.00 × Width
Cylinder — the rim between wall and lid45°0.71 × Width
Cube — corner normal (1,1,1)/√354.7°0.58 × Width

So one profile on four primitives draws four different thicknesses, with the box at little more than half the sphere. Nothing is misconfigured; it is what pushing along an averaged direction does.

Smooth Normals measures the correction each vertex needs — 1 / cos θ — and Miter Limit caps it, because on an acute spike that figure runs away and the point would shoot out into a needle. At 1 there is no correction at all, which is the behaviour in the table above.

The default, 1.45, sits just above the 1.41 a right-angled edge asks for. That is a deliberate line to draw: an edge is corrected exactly, and a point is not.

FeatureCorrection it wantsAt Miter Limit 1.45
Sphere, capsule, any smooth surface1.001.00 × Width
Cube edge, cylinder rim — two faces at 90°1.411.00 × Width
Cube corner — three faces at 90°1.730.84 × Width

So the long edges that make up almost all of a box's silhouette come out at exactly the Width asked for, and only the corner points themselves stay slightly tucked in — which reads as the outline easing round the corner rather than shooting past it. Raise it to 2 if you want the corner geometrically exact as well.

Upgrading. This correction used to be worked out and then thrown away by the Normal expand path, so Miter Limit changed nothing there whatever it was set to, and the default of 1 meant nothing was corrected anyway. Both are fixed, so outlines on hard-edged models get wider — up to the Width they were always asking for. A profile that was tuned by eye against the old thin result will want its Width brought down; one tuned on a character or a rock will not change at all.

7.2 Mixed: deciding per face

Mixed classifies each part of the mesh once, when the outline is built, and then expands solid parts along their normals and sheet parts in their own plane. The classification is a measurement, not a guess: rays are cast from each face to find how far it is to the other side of the surface, and anything thinner than the threshold is a sheet.

SettingWhat it does
Mixed Detect Mode Thickness casts one ray per face — cheaper, coarser. Multi Ray casts a small cone and takes the consensus, which is what a mesh with noisy normals needs.
Thickness thresholdIn centimetres. Thinner than this counts as a sheet.
FlatnessHow parallel the two sides must be before they count as one sheet.
Rays / Ray coneHow many rays in the cone and how wide it opens.
Min sheet facesRaise it when a few stray triangles on a solid model get classified as a sheet: an island has to be at least this many faces to count.
This runs when the outline is built, not per frame. It costs load time and memory, never frame time. How much load time depends entirely on the mesh: on the demo's props the pause is not measurable above the noise (see 14.4), on a dense character it will be — which is what baking is for.

7.3 Sheet Solidify

Classifying a sheet is half the job. The other half is that a sheet has no thickness at all, so seen edge-on there is nothing for an outline to be. Sheet Solidify gives the detected sheets a shell: a thickness across the surface and a spread within it.

SettingWhat it does
Sheet SolidifyOn out of the box. Off leaves Mixed's sheet detection and in-plane expand doing their work unassisted.
SeparateAlso on out of the box. On, the shell is sized by Thickness and Lateral; off, by the single Volume number. The inspector shows one pair or the other, never both — they are two ways of saying the same thing.
ThicknessHow far the shell is pushed to each side of the sheet.
LateralHow far it grows within the plane — the outline's width on the flat part.
VolumeThe one number that drives both, when Separate is off.
Thicken X / YPush the shell to one side instead of both — a leaf growing off a stem should thicken away from it, not swallow it.

7.4 Baking

The analysis is deterministic, so on shipping content it can be done once instead of on every load. Bake for Runtime on the component — or Tools ▸ Keyline ▸ Bake All Outlines In Scene for the whole scene — writes the prepared hull meshes to assets and points the component at them.

Worth doing when Mixed or Solidify is in use and the content is final. Not worth doing while the look is still being tuned: a baked mesh is a snapshot, and changing the settings that produced it means baking again.

8 · Cutout

With Cutout on, the outline follows the shape the object's texture leaves behind rather than the shape of its mesh. A fence plank whose alpha punches holes in it gets an outline around the holes; a leaf card gets an outline around the leaf, not around the quad it is painted on.

An outline following a punched alpha shape
The mesh is a single flat quad. Everything you see — the contour and the two interior holes — comes from the texture's alpha.

8.1 Setting it up

  1. The source material needs alpha clipping on, with a texture that has an alpha channel. That is the object's own material, not the outline's.
  2. Switch Cutout on in the profile. The outline reads the same texture off the source renderer — _BaseColorMap, _BaseMap or _MainTex, whichever the material has.

There is no threshold to set on the profile. The outline is cut where the material is cut — _AlphaCutoff on an HDRP Lit material, _Cutoff on anything else — and the distance field is built at that same level. There used to be a second number here and it was removed rather than kept: the field records where the alpha crosses a level and the material cuts the artwork at its own, so the two have to agree, and two numbers that must be equal is one number too many. Move the material's cutoff and the outline follows — the field notices and is rebuilt at the new level.

8.2 The distance field

A hull widens edges the mesh has, and a punched contour is not one of them: no vertex sits on it. So the rim has to be drawn by widening the alpha itself — and an alpha cannot be widened well.

It is very nearly binary: a texel is opaque or it is not. Sampling it in a ring around each pixel and keeping the pixel if anything solid was found does widen the shape, but every sample answers yes or no and none of them answers how far. The boundary cannot be resolved below one texel, so the edge steps. Worse, a ring of N samples is an N-gon — the widened shape has as many flat sides as there were samples — so the count has to climb with the rim's size on screen, making a wide rim both the most expensive case and the ugliest.

Keyline builds a signed distance field from that same alpha instead, once, at author time. The field stores what the alpha does not: how far each texel is from the edge, to a fraction of a texel. Drawing then costs one texture fetch per pixel at any width, the edge is smooth at any zoom, and corners come out round because the distance to a corner really is round.

A cutout outline on a flat card, close up
What that buys, on a flat card seen close up: the edge stays smooth at this magnification instead of stepping along the alpha's texel grid, and the corners come out round, because the distance to a corner really is round.

The field also carries the whole profile of the rim to every pixel, which is what lets Halo compute its falloff as a curve and Double cut its gap directly, instead of stacking one draw per concentric layer.

Where the fields come from

Nobody has to make them by hand.

The reference matters as much as the file. A build includes what something points at, and nothing else points at these — a cutout mesh's texture is found by reading its material at run time, which a build has no way to anticipate. That is why the field is stored on the component rather than looked up by name.

Range

Range is how far the field can describe, in texels, and it is a hard ceiling on rim width: past it the field only says "at least this far", which cannot place an edge, so the outline stops there. Reach is paid for in memory, not precision: the field is padded by its range on every side, so a 512×512 texture at Range 32 is written as 576×576. Sixteen bits of distance put a step far below anything a rim can show, whatever the range — see 12.2. Pick the smallest that covers your widest outline.

Rim Softness is how far the edge fades out, as a fraction of the rim's own width. 0 is as crisp as an edge can honestly be — one screen pixel, which is what antialiasing needs and no more — and 1 fades across the whole band, which reads as a halo rather than a drawn line. A fraction rather than a pixel count so that one setting looks the same on a two-pixel rim and a forty-pixel one.

It is also part of how far the rim reaches: the band fades to nothing across the feather, so the last visible thing sits about half a feather past the width. Set To Max on Width and the minimum Padding both allow for it, which is why raising Softness lowers the width the two of them suggest.

Under Width Mode: Pixels the same slider is drawn as Rim Falloff. It is still a fraction of the rim's own width — 0.5 fades the outer half of its texels and leaves the inner half solid — but the fade steps by whole texels, because that is the only unit a pixel rim has: the outermost ring is dimmest and each one inward is brighter. See 12.3.

A texture with no asset behind it — a render target, or one built at run time — cannot have a field. The rim then falls back to the artwork's own silhouette with no band around it, which is a visible outline rather than a missing one.

Layers

Layers is how many steps the falloff is built from, and it means the same thing on both subjects — which is the point. A mesh draws that many concentric hulls, and because they blend additively the brightness at any radius is the level of the nearest hull outside it: a five-layer halo really is five bands, not a ramp. A sprite quantises its distance the same way and gets the same five bands out of one draw.

What differs is the price. On a mesh every layer is a draw call, so the count stops at 8. On a sprite it costs nothing, so it goes to 32 — which is smooth enough to stop looking like steps at all.

8.3 What Cutout changes elsewhere

With Cutout on, the hull is not extruded at all and the whole Expand section stops applying — the inspector hides it. In-plane extrusion would slide the surface sideways under a UV that stays put, stretching the punched shape instead of outlining it; off-surface extrusion would separate the hull from the model, invisible head-on and a second parallel line edge-on. Width feeds the alpha ring instead.

Two more consequences worth knowing before shipping it:

8.4 Flat meshes

Cutout wants a flat mesh — fences, foliage cards, decals, a sprite texture on a quad. Single- or double-sided makes no difference; flatness does.

"A sprite texture on a quad" is not the same as a SpriteRenderer. Cutout reads the alpha of the material on a MeshRenderer, so a quad wearing a sprite's texture works exactly like a foliage card. A SpriteRenderer is a different renderer type and has its own component, Keyline Outline 2D — it measures the rim from the artwork's alpha instead of extruding a hull. Put Keyline Outline 3D on a sprite and the Inspector offers a button to swap it over. See Sprites.

On a solid mesh the alpha rim sits on the surface; the outer silhouette is a normal hull outline, or a Cutout Overlay on the same pass. Padding that widens a flat card in-plane is skipped on volumes — Cutout still traces alpha.

Experimental. Cutout on a solid or volumetric mesh, and Cutout Overlay itself, are experimental. Both can change in a later release. Cutout is meant for flat cards.

9 · Merge groups

Two objects side by side get two outlines, and where they overlap you see the seam between them. Sometimes that is right — two enemies are two enemies. Sometimes it is not: a character and the weapon in their hand are one thing, and a contour running through the middle says otherwise.

Separate outlines against a merged silhouette
The same three props, one switch apart: separate contours with seams where they overlap, and one contour around the group.

9.1 On the outline itself

Two switches under Setup on both outline components, on out of the box:

SettingWhat it joins
Merge Child Outlines The children, drawn inside this silhouette rather than beside it. It applies to any child that has an outline — one of its own, or one generated for it by Include Children. It is a statement about the children: whether this outline joins the one above it is that one's decision, taken with its own copy of the switch.
Merge When Overlapping Whatever this outline's bounds touch. Tested between figures, not between objects: a model that has taken its children into one silhouette merges on contact as a whole, so this switch covers those children too and something touching a child merges with the model rather than with the part it brushed. A figure merges when any part of it is willing, so two separate objects both need it on. The outline counts, not only the object — see the note below. Re-tested a few times a second, not every frame.

Both work identically on Keyline Outline 3D and Keyline Outline 2D. Overlap on a sprite is measured from the renderer's bounds, which is a rectangle: two sprites whose artwork does not touch can still have overlapping bounds.

The outline is part of the figure. Unity measures the object, and the outline is drawn outside it — so two bodies a rim-width apart would look separate to this test while their outlines were plainly touching. Each figure is therefore widened by its own reach, taken from the style that draws it: Double reaches past its inner band by the gap and the outer ring, Frost by its shell, and Pulse counts at the top of its swing so a cluster cannot come apart on the downbeat.

A Width Mode of Constant Screen is the exception: its width is a different distance in the world at every camera position, so counting it would make merging a property of where the camera is standing — silhouettes fusing and splitting as somebody walks towards them. Those bodies still merge on overlap; only the rim's own reach is left out. World on a model, and World or Pixels on a sprite, are all counted.

9.2 Explicit — Keyline ▸ Merge Group

For what automatic merging cannot see: things that stand near each other without touching, and things that must read as one unit however far apart they drift.

Put the component on a parent and, with Group Children on as it ships, every outlined object beneath it belongs to the group. Membership is then the hierarchy — reparent a member in or out at runtime and its controller notices by itself, with nothing to be told. The group's stencil pair is allocated and released by enabling and disabling the component.

Two lists qualify that. Members names outlines one at a time, which is how a group gathers objects that are not under it; naming any member at all is also what turns the group into a list rather than a subtree, so unnamed objects underneath stop being swept in. Ignored does the opposite for one child: it keeps its own outline instead of joining the silhouette.

9.3 How it is decided

Not "which rule wins" — all of them apply at once. Belonging to a silhouette is transitive: if A is with B and B is with C, then A is with C. Each rule adds a link, the links combine, and every outline in one resulting set is drawn as one silhouette.

RuleLinks
Merge Groupevery member of one active group
Merge Child Outlines an outline and the one above it, when that one takes its children
Merge When Overlapping two outlines whose bounds touch, when both are willing

Because links combine rather than compete, the order they are found in does not matter — which is what makes the result predictable. A model inside a group brings its children in with it: the child is linked to its parent, the parent to the group, so all of them are one silhouette and all of them wear the group's stencil.

Overlap is vetoed where somebody has already spoken. A parent that does not take its children walls that branch off, and a group that names its members by list keeps two unnamed objects under it apart. A Merge Group and a parent are statements a person made; overlap is a guess from geometry, and a guess does not overrule a person. The ignore list takes an object out of all three.

Reading the result. Press Log Pass Diagnostics on an outline and it prints the silhouette it resolved to. Two outlines showing the same value are one silhouette; two showing different values are not.
Sprites: the orders have to differ. Merging is done with a stencil — every outline stamps the shape its neighbours must not draw over, and the rims reject that stamp. Within one sorting layer the only thing deciding who draws first is Order In Layer, so two overlapping sprites sharing a layer and an order have no defined sequence: one rim can be drawn before the other sprite has stamped what it must avoid, and the seam survives.

None of the merge settings can repair that, because it happens below them. Give the sprites different Order In Layer values, or put them on different Sorting Layers. Their Sorting Order Offset counts too: two sprites on the same order whose offsets differ do not collide, because the offset is what moves each rim off its own sprite.

Duplicating a sprite is the usual way into this — a copy inherits both numbers exactly. The component and the Merge Group both say so in the Inspector when they see it.

9.4 What it costs

A group spends one stencil pair for the whole group, however many members it has — see 3.4. On a crowd that is the cheaper option, not the more expensive one: eight soldiers in a squad cost what one object costs, so the 126-pair budget starts counting squads instead of soldiers.

10 · Skinned meshes

A skinned mesh needs no special handling: add the component and the outline follows the animation.

The hull is a second SkinnedMeshRenderer pointed at the same bones and the same root. Unity skins it in exactly the same pass it skins the original, so there is no per-frame work of Keyline's own — no baking a mesh every frame, no copying vertices, nothing that scales with the animation's complexity. What it costs is one more skinned draw per pass, which is what any second renderer costs.

An animated character with an outline
Mid-animation. The hull is skinned by the same pass that skins the character, so the outline costs nothing per frame beyond one more skinned draw.
Sheet Solidify does not apply to skinned meshes. It works by rebuilding the hull's topology, and rebuilding triangles on a skinned mesh would break the bone weights that make it move. Mixed's per-face decision still applies; only the topology rebuild is skipped.

In practice this rarely matters: characters are volumes, and the flat-sheet machinery exists for props. Where a character does carry a genuinely flat part — a cloak, a cape, a paper charm — give that part its own renderer and its own profile.

11 · Recipes

Thirteen entries under Add Component ▸ Keyline ▸ Recipes, from the files in Runtime/Recipes/ — twelve recipes plus the arbiter that keeps them from fighting over the same object. They are ordinary components written against the same public API you have — nothing in them reaches into the outline through a back door — and they exist because the same five or six arrangements get written from scratch by everyone who buys an outline asset.

Every one of them is shown in the demo's Interaction chapter, one mini-scene each.

ComponentSolves
KeylineHighlightseveral sources wanting the outline at once
KeylinePointerHighlighthover
KeylineClickSelectionclick to select, shift to add
KeylineTriggerHighlightentering a volume
KeylineProximityHighlightgetting close
KeylineDamageFlasha hit landing
KeylineFillGaugea level painted on the model
KeylineImpactWavea ring from the point of impact
KeylineScanWavea front crossing the scene
KeylineRevealan object materialising or dissolving
KeylineGroupHighlighta squad answering as one
KeylineXRayWhenHiddenthrough-walls only while actually hidden. Was KeylineOccludedXRay, which still compiles as a deprecated subclass
KeylineBlinkattention, briefly
KeylinePointerone pointer API over both input systems
KeylineTriggerRelaytrigger callbacks reaching a parent
IKeylineHighlightTargetone object or a group, told apart by nobody

11.1 What every timed recipe shares

Five of them run for a while and then stop: Blink, Damage Flash, Impact Wave, Scan Wave and Reveal. They answer to the same three members and raise the same two events, so code that drives several of them does not have to remember which is which.

Member
Play(...)starts it. Every recipe also keeps the name that describes what it actually does — Emit for a wave, Ping for a scan, Flash for a hit. Neither name is preferred: Play exists because it is the first thing most people type, the specific name because it reads better in your own code
Stop()ends it early and puts the slot back. Calling it when nothing is running does nothing
IsPlayingtrue while it runs. Damage Flash also answers to IsFlashing, which is the same value
StartedUnityEvent, fires on every start — including one that restarts an effect already running
FinishedUnityEvent, fires when it ends by any route: ran its course, Stop, or the object being disabled. It does not fire for a Stop that had nothing to stop

Both events are on the component in the Inspector, so a designer can wire the next step — a sound, a spawn, another recipe — without writing anything.

Reveal is the exception worth knowing. Its Stop() means finish, not cancel: a reveal aborted halfway would leave the object half materialised, which is a state nobody asked for. It completes and raises Revealed exactly as it would have on its own. Finished always fires after Revealed or Dissolved, never instead of them.
Five of them force the Gradient style on the pass they paint. Damage Flash, Fill Gauge, Impact Wave, Scan Wave and Reveal are all built on the Gradient — the travelling band, the fill level and the directional falloff are its parameters, and nothing else can express them. So if the profile in that slot is set to Neon and the wave draws as a gradient, that is by design and not a bug. The style is restored along with everything else when the effect ends. Passes you are not driving are untouched, which is the usual reason to put an effect on the interior fill and leave the rim to your own profile.
One pass, one writer. Damage Flash, Fill Gauge, Impact Wave and Reveal all default to Main Interior, and each of them drives the whole profile of the pass it paints. Two on the same pass will overwrite each other: whichever finishes first restores the slot from the shared profile and erases what the other was drawing — which looks like a bug in the one that survived. Keyline warns about this in the Console when the object is enabled, naming the components and the pass. The fix is always the same: give them different passes. There are four, they are independent, and each carries its own profile.

11.2 Highlight — the arbiter

Three scripts writing the same colour is the bug everybody writes once. Hover the object, select it, move the pointer away — and the hover release clears the selection, because the last writer wins and nobody agreed who the last writer should be.

KeylineHighlight holds named states with priorities. Anything can push a state and pop it later; the highest priority wins, and releasing it falls back to the next one still held rather than to nothing.

var highlight = target.GetComponent<KeylineHighlight>();

highlight.Push("Hover");      // priority 10
highlight.Push("Selected");   // priority 50 — this is what shows
highlight.Pop("Hover");       // still selected

Each state carries its own colour, width multiplier and fade time. Every highlight recipe below speaks to this component rather than to the outline, which is why they compose.

11.3 Pointer Highlight and Click Selection

Both live on one object in the scene, not on the props. One raycast per frame answers for everything, where a script per prop would cast one ray per prop to answer a question that has a single answer.

A prop takes part by having a collider and a KeylineHighlight. Nothing is registered, nothing is wired, and the ray looks past colliders with no highlight on them — so a hitbox in front of the model does not swallow the hover.

Pointer Highlight
Cameraempty uses Camera.main
State Idwhich state to push; default Hover
Layers, Max Distancewhat the ray may hit and how far
Blocked By UIignore hovers while the pointer is over uGUI
Prefer Groupsresolve to the group a prop belongs to, not the prop
Click Selection
State Iddefault Selected
Max Selection1 is classic single select; 0 is unlimited
Clear On Empty Clickclicking nothing clears
Toggle With Modifiershift or ctrl adds and removes

Selection is a held state, not a colour written into the object — which is why selecting something the pointer is also over does not fight with the hover.

11.4 Trigger and Proximity Highlight

Trigger Highlight lights the object while something is inside a trigger collider — a zone, a doorway, a pickup radius. Filter by layer and by tag.

Unity delivers OnTriggerEnter to the collider's own GameObject, not to a parent. KeylineTriggerRelay is added automatically when the zone sits on a child, and forwards the callbacks up. Worth knowing because it is the reason a trigger on a child works at all.

Proximity Highlight needs no collider: it measures distance to a target — assigned, or found by tag — and can fade the highlight between a near and a far radius rather than switching it. The check runs on an interval, not every frame.

11.5 Damage Flash

A critical hit flash
Peak of a critical hit. The flash lives on the interior fill, so the rim keeps whatever it was doing.

A hit, drawn on the model through the interior fill. Two intensities out of the box — Flash() and FlashCritical() — with their own colours and glow.

var flash = enemy.GetComponent<KeylineDamageFlash>();

flash.Flash();                        // ordinary hit
flash.FlashCritical();                // heavier colour, brighter
flash.Flash(hit.point);               // the flash starts where it was struck

The directional form takes a world point and has two shapes: a falloff spreading from the point of impact, or the struck half of the object lit and the other half left alone. The axis is built from the outline's bounds centre rather than the transform, because a prop's pivot is usually at its feet.

API
Flash() · Play()ordinary hit at full strength
Flash(float strength) · Play(float)pass damage over max health and a scratch reads differently from a heavy hit for free
Flash(Vector3 worldPoint, float strength = 1f) · Play(Vector3, float)directional — brightest where it landed
FlashCritical() · FlashCritical(Vector3)the heavier colour and glow. Its own name because it is a different effect, not a different signature
Stop()cut the flash and restore
IsFlashing · IsPlayingthe same value under both names
Directional, Falloff, FalloffSoftnessshape of a directional hit, at runtime
Started, FinishedUnityEvents

Default pass: Main Interior — the fill is what reads as the body being hit, and it leaves your rim profile alone.

The usual mistake: putting Damage Flash and Fill Gauge on the same object without changing the pass on one of them. Both default to Main Interior, and the gauge level resets every time a hit lands. See 11.1.

11.6 Fill Gauge

A gauge drawn on the model
The level is a boundary sliding across the surface, not a bar over the head — it rides the object because it is drawn on the object.

Health, charge, build progress — drawn on the object rather than over its head. Nothing is masked, clipped or re-meshed: the level is one shader parameter, so it costs the same at any value and animates for free.

var gauge = enemy.GetComponent<KeylineFillGauge>();

gauge.SetValue(health, maxHealth);    // or SetValue(0..1)
gauge.SnapTo(1f);                     // no animation

Calibration is the part to read. The sweep runs across the mesh's bounds, and a model that does not fill its bounds evenly — greens on top, a tapering root below — reaches empty and full well away from the theoretical −0.5 and 0.5. Set the level to 0 and drag Offset At Empty until the fill just vanishes, then the level to 1 and Offset At Full until it just covers the model.

Tint When Low shifts the fill towards a warning colour below a threshold — a level and a warning are different pieces of information, and a bar that is only shorter makes you work the second one out. Smoothing is how long the drawn level takes to catch up: 0 snaps, and a quarter of a second turns a number changing into a hit landing.

API
Valuethe target level, 0 to 1. The drawn level eases towards it
SetValue(float) · SetValue(float current, float max)the same thing as a method, for UnityEvents and for raw health numbers
SnapTo(float)set and draw immediately, no easing — for spawning at full health
DisplayedValuewhat is actually on screen this frame, which lags Value by Smoothing
Angledirection the gauge fills, 0 to 360. 0 fills bottom to top
FillColor, EmptyColor, EdgeSoftnesslook, at runtime
TintWhenLow, LowColor, LowThresholdthe warning shift
OffsetAtEmpty, OffsetAtFull, CurrentOffsetcalibration, described above

Default pass: Main Interior. This is the one recipe that holds its pass continuously rather than for the length of an effect, which is why it is the one most likely to be disturbed by another recipe sharing the pass.

The usual mistake: leaving the calibration at its defaults on a model that does not fill its bounds evenly, then concluding the gauge is inaccurate. Calibrate first — it takes two drags and it is per model.

11.7 Impact Wave and Scan Wave

Both are built on the Gradient style's world-space shapes, and both use the same two colours: Core at the middle of the front and Edge everywhere else.

The band is symmetric. Edge is not a trailing colour — it is what the object looks like everywhere the front is not. Give it alpha 0 unless you want the whole model tinted while the wave is somewhere else entirely.

Impact Wave is per-object: a ring spreading from a point you give it, with its own speed, reach, thickness and a fade in / hold / fade out envelope.

Scan Wave lives on one object in the scene and drives a list of controllers: a sphere growing from a point, or a plane travelling along an axis, crossing everything in its path in step. It writes only to the objects the front is actually touching, so a scan across a room does not cost anything on the objects it has already passed.

Leave its target list empty and it collects every controller in the scene the first time it runs. InvalidateTargets() makes it collect again — call it after spawning.

Impact Wave
Emit(Vector3 worldPoint) · Play(Vector3)from a point — usually where the hit landed
Emit(RaycastHit) · Play(RaycastHit)straight from a raycast
Emit() · Play()from the object's own centre
Stop(), IsPlayingas in 11.1
Speed, Reach, Thickness, EdgeSoftnessthe front, in metres and metres per second
FadeIn, Hold, FadeOutthe envelope. Total life is the three added up
CoreColor, EdgeColor, Glowlook, at runtime
Scan Wave
Ping(Vector3 origin) · Play(Vector3)send a front out from a point in world space
Ping() · Play()from this object's own position
Stop()ends it and puts every object it touched back as it was
Distancehow far the front has travelled, in metres
Shape, Axissphere or plane, and which way a plane faces
Speed, Range, Thickness, EdgeSoftnessthe front, in metres
InvalidateTargets()rebuild the target list from the scene on the next ping

Default pass for both: Main Interior.

if (Physics.Raycast(ray, out RaycastHit hit))
{
    var wave = hit.collider.GetComponentInParent<KeylineImpactWave>();
    if (wave != null) wave.Emit(hit);   // or wave.Play(hit)
}
Which one to reach for. Impact Wave is per object and starts where you tell it. Scan Wave is one component for the whole scene, and its front is measured in absolute world space so that a hundred objects answer the same question about the same origin — which is what keeps it a single wave instead of a hundred small ones. If you find yourself putting an Impact Wave on every prop and firing them together, you want Scan Wave.

11.8 Reveal

An object materialising
Mid-climb: the fill has reached partway up the silhouette and the mesh is not there yet.

An object arriving. The mesh starts hidden, the fill climbs the silhouette, and when it reaches the top the outline flares and the real mesh appears under the flare.

var reveal = spawned.GetComponent<KeylineReveal>();

reveal.Play();          // materialise
reveal.PlayReverse();   // dissolve, flash first

The flare is not decoration. Drop it to zero and the swap becomes visible: the fill vanishes and the lit mesh appears in its place, and the two never quite match. Hiding that is the only reason it is there.

API
Play()materialise
PlayReverse()dissolve. Its own name because it is the opposite behaviour, not another signature
Complete() · Stop()end now, with the object present. See the note in 11.1
IsPlayingtrue during either direction
Revealed, DissolvedUnityEvents, one per direction
Started, FinishedUnityEvents, either direction
ClimbSeconds, FlareSeconds, FlareGlowtiming and the flare
FillColor, EdgeSoftnesslook of the climbing fill
OffsetAtEmpty, OffsetAtFullcalibration, same idea as Fill Gauge

Default pass: Main Interior. Play On Enable is on by default, so a prefab dropped into the scene materialises by itself; turn it off if you want to trigger it yourself.

The usual mistake: expecting the object to be hidden before the first frame. Reveal hides the body in Start, not in Awake, so that code which adds the component and then configures it is not racing a run that already began. Spawn the prefab inactive if a single frame of visible mesh matters.

11.9 Group Highlight

One squad highlighted as a single silhouette
Point at one member and the squad answers — as one silhouette, and for one stencil pair.

Put it on the parent of a group and the members answer together: point at one, the squad lights up. With Merged on, the group draws as a single silhouette with no seams where the bodies overlap — and spends one stencil pair for the whole squad rather than one per body, so the budget in 3.4 counts squads instead of soldiers.

Membership is the hierarchy. Reparent a member in or out at runtime and its controller notices by itself.

11.10 X-Ray When Hidden

Left on permanently, a through-walls outline stops meaning anything: it looks the same when the objective is in plain sight and when it is behind a building, so the player learns to ignore it. This component ties the X-Ray pass to real occlusion — the marker appears exactly when the thing it marks disappears.

Occlusion is tested with a line cast from the camera, on an interval, and the object's own colliders are skipped. There is a short clear delay so walking past a railing does not make the marker strobe, and separate fade-in and fade-out times, because appearing is the information and disappearing is only the absence of it.

Include Interior Fill switches the X-Ray fill on with the rim: a filled silhouette reads across a level, a rim alone is quieter and scales to more objects at once.

This was KeylineOccludedXRay in 1.0.0. Updating breaks nothing: components already in scenes and prefabs keep working, and the old name still compiles — it lives on as a subclass of this one, so it is this component under either name rather than a wrapper around it. The compiler marks each use as obsolete so you can rename at your own pace. New objects should be given X-Ray When Hidden; the old name is hidden from Add Component.

Pushes and pops a state on a timer — on for so long, off for so long, so many times, or until stopped. For a quest object that should be noticed once rather than outlined forever.

blink.Play();      // the authored count
blink.Play(3);     // three times
blink.Stop();      // and release the state
API
Play()start with the count set in the Inspector. 0 means until stopped
Play(int count)start with a count given here, whatever the Inspector says
Stop()stop and release the state
IsPlaying, StateIdstate of the run, and which highlight state it holds
Started, FinishedUnityEvents

Unlike the other four, Blink drives the arbiter rather than a pass of its own — it pushes and pops a named state. That is what lets a blinking object stay selected underneath, and it is why Blink needs a KeylineHighlight on the same object while the others need a KeylineOutlineController.

The usual mistake: pointing Blink at the same state something else is already using — Hover, most often. The pointer leaving the object pops that state and cancels the blink. Give the cue a state of its own, and give it a priority above the states it has to show through.

12 · Sprites

A SpriteRenderer has its own component, Keyline Outline 2D. Everything about the look is shared with meshes — the same profile assets, the same styles, the same four passes — but the outline is made a different way, and that difference decides most of what follows.

A mesh outline is an inverted hull: the geometry is pushed outwards and the silhouette does the rest. A sprite has no hull to push. Instead the rim is measured against a distance field built from the artwork's alpha, so the outline follows what is drawn rather than the quad it is drawn on — a hole in the middle of a sprite gets an outline, and so does every notch along its edge.

A character sprite behind a fence, outlined and showing through
Three passes in one frame. Where the character is in the open, Main Outline traces the artwork — the drawn silhouette, not the quad it sits on. Where the fence covers him, the same silhouette continues as X-Ray Outline with X-Ray Interior filling it, and the two hand off where the fence begins. The X-Ray rim here is softened with Rim Softness — a sprite fades its edge in the distance field it is measured from, not through the screen-space Soft Edge pass, which a field has no need of. The fence is an ordinary sprite; what makes it an occluder is that it was declared one.

12.1 The component

Add Keyline Outline 2D to the object, press Add pass, and assign a profile to Main Outline. Extra passes — Main Interior Fill, X-Ray Outline, X-Ray Interior Fill — are added the same way as on the mesh component, and the × on a card hides the row and turns the pass off while leaving the profile assigned. If you added the mesh component by mistake, its Inspector offers a button to swap it over.

The pass rows, the profile slots and everything under them are the mesh component's, unchanged; what follows is only what a sprite has instead of a hull.

FieldDefaultWhat it does
Padding0.25 Empty room around the sprite for the rim to be drawn on, in the sprite's own units. The rim can only appear where the quad has surface, so too small a value cuts the outline off in a straight line wherever it would have gone further — which reads as a corrupted outline rather than as a number being too small. Set Minimum beside the field works out exactly enough for the widest pass this object draws.
Sorting Order Offset1 How many sorting orders away from the sprite the passes are placed. The main outline is drawn under the sprite as a solid silhouette, so the artwork covers its middle and no seam can appear along an antialiased edge. Two overlapping sprites that share a Sorting Layer, an Order In Layer and this offset cannot merge — see 9.3. Give one of them a different order.
Fieldfound The distance field the rim is measured against. Filled in automatically when one is sitting beside the sprite; built with the button below when there is not. It is a serialized reference rather than a name looked up at run time, because that reference is what pulls the texture into a player build.
Field ModeSmooth What the field is measured with. Smooth for painted and vector artwork, Pixel Art for artwork meant to be seen texel by texel. Changing it needs the field rebuilt — the rounding is in the numbers, not in how they are read. See 12.3.
Range (texels)32 How far the next field can describe, and a hard ceiling on rim width: past it the field only says "at least this far", which cannot place an edge, so the outline stops growing while Width goes on rising. Reach costs memory, not precision or speed — the field is padded by its range on every side, so a 32×32 sprite at Range 32 is written as 96×96.
Include Childrenoff Outlines every sprite under this transform, so a multi-part character reads as one object. Off out of the box, unlike the mesh component: a 2D figure is usually several sprites drawn in a deliberate order, and quietly taking them all over is a larger decision than it looks. Ignore Children List is the exceptions.
Instance Overrideon Runtime writes fork the profile into a copy this object owns rather than editing the shared asset — the same rule as on meshes. See 3.3.
The Keyline Outline 2D inspector
The 2D component. The pass rows at the top are the mesh component's, unchanged — the same profile slots, the same four passes. What is different is underneath: the padding the rim is drawn on, with Set Minimum beside it, and the distance field the rim is measured from, with the range and metric it actually holds stated rather than assumed.

The Inspector reports what it knows rather than leaving it to be discovered: the range and metric the assigned field actually holds, the resolved rim width in texels, and a warning with a fix button for each of the handful of settings that quietly undo an outline.

12.2 The distance field

Under Distance Field, press Generate Distance Field. Nothing is built automatically: the file is written next to the sprite's texture and named after it, and writing into somebody's project is a decision rather than a side effect of selecting an object. One field is built per texture and shared by every sprite using it, so a sheet holding sixty frames is one file.

The range, the threshold, the metric, the precision and the scale it was measured under all travel in the file's name. A field is a table of distances scaled by one number, and read back with a different one every distance in it is wrong by that ratio — while the picture still looks perfectly well-formed. Nothing about the pixels reveals the mistake, so the numbers live where they cannot drift away from the file.

Padding

Every field is baked on a canvas larger than the artwork, padded on each side by the range. The rim is drawn outside the sprite by definition, and a field that stopped where the artwork stops has nothing to say about the ground the rim is standing on. Where the drawing runs to the edge of its own texture there is no honest answer to extrapolate — the distance travelled past the border is added as a radius, so a wide outline grows a round bulge along what is a perfectly straight side in the artwork.

So a 32×32 sprite at Range 32 produces a 96×96 file. Lower the Range before generating if that matters; an outline of one or two texels does not need thirty-two.

Power Of Two beside the range grows the canvas to the next power of two on each axis. It makes the file larger, not smaller — a source already at a power of two crosses to the next one as soon as any margin is added, so 128 becomes 256 where the plain padding would have stopped at 192 — and it is off by default for that reason. Tick it where something downstream requires the guarantee. Block compression is not that something: it needs sides that are multiples of four, which these already are, and it would ruin a distance field anyway, since it works by discarding exactly the small local differences the field is made of.

Precision

Fields are written with sixteen bits of distance, split across two channels — the high byte in red, the low byte in alpha. One byte gives 256 steps across twice the range, so at Range 32 a step is a quarter of a texel and at 128 it is a whole one; a whole texel of error is the size of the detail the field exists to resolve, and it shows on a wide smooth rim as faint terracing. The second byte removes it.

The low byte swings across its whole range every quarter texel, which is why it is in alpha rather than in a colour channel: in green it turned the file's thumbnail into static, and a texture that looks corrupted is one that gets reported as corrupted. Fields written before this carry no marker in their name and are read back as the eight-bit fields they are.

Stretched sprites

A sprite scaled (2, 1, 1) has texels twice as wide as they are tall in the world. A field measured on the texel grid knows nothing about that, so a rim of three texels is three texels in both directions — which is twice as thick down the sides as it is across the top. Nothing downstream can undo it: the shader reads one number per texel, and the direction that number was measured in is exactly what a distance field throws away.

So a smooth field is measured on the sprite's own stretched grid instead, automatically, and the rim comes out even. Width keeps its meaning — the arithmetic is normalised so that a Width of W is still a world radius of W, rather than the rim becoming thicker or thinner as a side effect of being evened out.

Only the ratio of X to Y matters. A sprite at (3, 3, 1) has square texels exactly as one at (1, 1, 1) does and needs nothing done to it. The Inspector says which ratio the next build will be measured for, and warns when a field's ratio and its sprite's have drifted apart — change the scale after baking and the field has to be rebuilt, which it says rather than leaving a rim that is subtly uneven.

A pixel field is left square, and warns instead. Weighting the grid metrics is possible, but a pixel rim is a whole number of texels on each axis and at the widths pixel art is drawn at there is rarely a pair of whole numbers in the right ratio — at a Width of one texel the narrow axis wants half of one. The two available answers are a rim missing on two sides or one that is too thick, so the Inspector points at the scale instead. Make X and Y equal; any value will do.

Animation

An animation whose frames all live on one sheet needs nothing extra. One whose frames are spread across several textures needs a field per texture, and the Animation section takes the clip and builds them all: its sprite keyframes name every texture the animation will ever show, so the set is a read rather than a search. The outline swaps fields as the frames change, so the rim is always measured against the picture actually on screen.

Sprite Atlas

Supported for Full Rect packing without rotation. The sprite draws from the packed page while the field still covers the source sheet, and the mapping between the two is measured from the sprite's own rectangle. Tight packing or rotation leaves no rectangle to map through: the rim falls back to the silhouette and says so once in the console. An animation that jumps between source sheets swaps fields with the frame, packed or not, as long as each sheet has an entry in Fields — Generate on the clip records them.

12.3 Pixel art

A smooth field measures real distances and resolves its edge below a texel. That is the right answer for painted and vector artwork and the wrong one for pixel art, where the boundary between two texels is the artwork and rounding a corner is a mistake rather than a nicety.

Set Field Mode to Pixel Art and rebuild. The field is then measured on the texel grid, so the edge lands on texel boundaries and corners come out square — an outline drawn the way the artist would have drawn it by hand.

The same pixel art sprite outlined by a smooth field and by a pixel field
The same sprite, the same profile, the same width — two fields. On the left the distance is measured as a real distance, so the edge sits between texels and the corners are round. On the right it is measured on the grid: every step of the outline is a whole texel of the artwork, which is what the artwork is made of.
CornersA diagonal step costsReads as
8-wayone the outline follows staircase edges and closes around single-texel corners. What most pixel art wants, and the default.
4-waytwo no outline across a diagonal join and outer corners cut back by one texel — a thinner contour, useful on small sprites where 8-way reads as heavy.

Both are baked to their own file, so a sprite can carry one of each and the Corners switch moves between them without rebuilding anything.

8-way and 4-way outlines on the same staircase edge
8-way and 4-way on the same staircase. 8-way closes around every corner and follows the diagonal; 4-way steps back from it, which costs a texel at each corner and reads as a lighter contour. Neither is a correction of the other.

Width is counted in texels

Set the profile's Width Mode to Pixels. Width is then a whole number of source texels: 1 is a one-texel outline, on every sprite and at every zoom.

Neither of the other two modes can express that. Metres go through Pixels Per Unit, so the same profile draws three texels on one sprite and ten on another and the number in the field means nothing on its own. Screen pixels go through the camera, so an outline authored as two texels is two and a bit at one zoom and one and a half at the next — which is the definition of not being pixel art.

Switching the mode does not convert the number. The factor is a sprite's Pixels Per Unit and a profile is worn by however many sprites happen to wear it, so converting against one of them would rewrite the width for all the others. The Inspector says what it did and leaves a sensible value. Pressing Edit on a profile from a sprite that is drawing a pixel rim is the one exception: a particular sprite is in hand there, so the width is converted and the profile opens showing the outline already on screen.

What the artwork needs

The component checks all of these and says which one is in the way. It also offers the switch when it sees point-filtered artwork under a smooth field — an offer rather than a change, because an import setting is not a statement of intent.

Edge Softness does nothing here. An edge that lands on the grid is the point of the mode, and feathering it in screen pixels would be a soft edge arrived at by the back door. It is ignored while a pixel rim is drawn, and the Inspector says so.

Rim Falloff

Under Width Mode: Pixels the profile's softness slider is drawn as Rim Falloff (texels) and does something a grid can honestly do: it fades the outline a whole ring at a time, dimmest on the outside, brightening inward. The value is the share of the outline's own width that fades — 0 is a solid rim, 0.5 fades the outer half of its texels, 1 fades across all of them — so one setting reads the same on a two-texel rim and a twenty-texel one.

A one-texel rim has nothing to be dimmer than, so it simply takes the value as transparency.

Pulse

Pulse works on a pixel rim, and it steps. A smooth rim's edge is placed by a threshold on a continuous distance, so the geometry breathing is the effect; a pixel rim's edge is placed by a texel count, and a count can only move a whole texel at a time.

Two controls shape what that reads as, and both appear on the profile when Pulse is on:

12.4 X-Ray needs an occluder

In 3D the depth buffer answers "is this pixel behind something", and X-Ray is built on that question. A 2D scene leaves that buffer empty, so Keyline supplies the missing input: an occluder writes a depth value derived from its place in the sorting order, and the existing machinery works unchanged.

Nothing else writes it. Without an occluder in the scene, X-Ray has nothing to draw against — the pass is configured, costs nothing and shows nothing. There are two ways to declare one:

Both of these say who occludes, and that stays a decision for the whole scene. It has to be: the depth an occluder writes is shared, so a proxy built for one sprite hides everything behind it, not only the sprite that asked for it. A per-object "which occluders apply to me" would be describing something it cannot control.

Which of those occluders a given outline can be seen through is a separate question, and that one is per outline. It is answered by X-Ray Sees Through on the sprite's own component, using GameObject layers rather than sorting layers — see 3.5. So a sprite can be an occluder for the scene and still let one particular character's X-Ray through, which sorting layers cannot express: a sorting layer is one number shared by everything drawing against it.

The registry sweeps the scene on an interval. A project that knows when its occluders appear can switch Auto Rescan off and say so directly:

layers.Add(spriteRenderer);      // one sprite, no scene sweep
layers.Remove(spriteRenderer);   // let one go
layers.Rescan();                 // sweep now

An occluder must rank above the sprite it hides. The depth value comes from sorting position — sorting layer first, then order within it — and the comparison is strict: an occluder sharing a sprite's layer and order does not hide it. Duplicating an occluder is the usual way to meet this, because a duplicate inherits both numbers. Give it a higher order, or a sorting layer further along. Both Occluder inspectors have a Log Occluder Diagnostics button that prints every live occluder, its rank, and whether each X-Ray sprite shows through it.

12.5 Limitations worth knowing

13 · Scripting

13.1 The everyday API

using FourSsets.Keyline;

var outline = target.GetComponent<KeylineOutline>();

outline.MainOutlineEnabled = true;              // switch the rim on
outline.Color = Color.red;                      // recolour it
outline.Width = 0.04f;                          // metres, in World width mode
outline.EdgeSoftness = 2f;                      // pixels; 0 is hard. Needs the Soft Edge pass.
outline.XRayEnabled = true;                     // show it through walls too

That is most uses. The properties on the component cover every field of the profile — colour, width, style, glow, the gradient, the halo, the lot — and they all write through the same guard described below.

The component is KeylineOutline. KeylineOutlineController is its base class, where the implementation lives; you can type against either, and the properties are the same. Add KeylineOutline, though — the attributes that make it behave like a component, ExecuteAlways among them, are on the concrete class.

13.2 Instance Override

Writing to a profile at runtime writes to a shared asset — see 3.3. With Instance Override on, the first write through a component property forks that slot into a copy this object owns. It is lazy: an object that only reads never allocates anything.

// Safe: goes through the property, forks on first write.
outline.Color = Color.red;

// NOT safe: the profile is a plain object here and nobody is watching.
outline.Settings.color = Color.red;   // every object sharing this profile turns red

When a whole batch of fields is changing at once, ask for the copy and write to it directly:

KeylineOutlineSettings mine = outline.ForkSlot(KeylinePassKind.Primary);

mine.color = Color.red;
mine.width = 0.05f;
mine.glowIntensity = 3f;
mine.ValidateValues();
outline.RefreshAppearance();          // push it without rebuilding geometry
MemberDoes
InstanceOverrideon by default; turning it off does not un-fork what is already forked
ForkSlot(kind)fork one slot now and hand back the copy
ForkAllSlots()all four at once
IsSlotForked(kind)whether this slot is running on a copy
SharedSlotSettings(kind)the asset the slot held before it forked — the look to return to
RevertSharedSettings()put the shared assets back and destroy the copies

RevertSharedSettings() is the cheap way to undo everything gameplay did to an object's look: rather than tracking which of forty fields were touched, drop the copy and the original asset is the look again.

13.3 Driving the passes

Switches and profiles, addressed by KeylinePassKind:

outline.SetPassEnabled(KeylinePassKind.XRay, true);
bool on = outline.IsPassEnabled(KeylinePassKind.MainInterior);
KeylineOutlineSettings fill = outline.PassSettings(KeylinePassKind.MainInterior);

outline.OutlineEnabled = false;                 // the master switch, above all of them

KeylinePassKind names six passes: Primary, XRay, MainInterior, XRayInterior, and — on a mesh only — MainCutoutOverlay and XRayCutoutOverlay. A sprite has four mirrors and no overlay pass, so it answers false for those two whatever you set, and SetPassEnabled on them does nothing. That is not the same as an empty slot, whose switch keeps whichever position you gave it — it is a pass that does not exist. The alternative, a default branch quietly treating them as the visible rim, is the silent wrong answer that makes generic code look like it worked.

Switch position or what is on screen

IsPassEnabled is the position of a switch. Set it and read it back and the two agree, which is what a save system needs. A pass whose switch is on but whose slot is empty draws nothing and still answers true.

bool switchedOn = outline.IsPassEnabled(KeylinePassKind.XRay);   // the switch
bool visible    = outline.IsPassDrawing(KeylinePassKind.XRay);   // switch, slot and master

Editing a pass that is not the active one

Every member that changes a pass names it in the call, with one exception: SetStyle takes a style and nothing else. An edit scope is how that one is told which pass it means — and, because it holds the rebuild back until it closes, how a burst of changes costs one rebuild instead of one each:

using (outline.EditScope(KeylinePassKind.XRay))
{
    outline.SetStyle(KeylineStyle.Neon);
    outline.Color = new Color(1f, 0.55f, 0.1f);
    outline.Width = 0.03f;
}   // one rebuild here, not three

Scopes nest, so a helper may open its own inside a caller's; only the outermost close does the work. BeginSettingsEdit and EndSettingsEdit are the same pair by hand — prefer the using form, because a throw between the two leaves the outline with a scope open for ever and every later change silently deferred.

Refresh or rebuild

Most changes are values the shader reads — colour, opacity, glow, gradient offset. Those need RefreshAppearance(), which is cheap and can run every frame.

Some changes alter how many hulls exist or how they are built: the style (Halo's layer count, Double's rings), the expand mode, Solidify, the feature mask. Those need Rebuild(), which throws the mirrors away and makes them again. The component notices this by itself when you go through its properties — the distinction matters when you write to a forked profile directly.

13.4 InvalidateCutout

Cutout resolves the texture off the source renderer's material and caches it against that material's instance id. Swapping the whole material is noticed by itself. Writing a different texture into the same material is not — the id has not changed, and re-resolving on every appearance push would put a property lookup per renderer on a path that runs whenever anything moves.

// A damage state, a variant, a seasonal skin:
mat.SetTexture("_BaseMap", damagedTexture);

// Tell the outline to look again.
outline.InvalidateCutout();

It clears the cache for every pass — the four ordinary ones and both Cutout Overlays — and refreshes. A texture swap is a fact about the source material rather than about one pass, and the X-Ray rim reads the same texture as the Main one.

13.5 Adding the component at runtime

Adding the component and giving it a profile is the whole of it, in either order:

var outline = enemy.AddComponent<KeylineOutline>();
outline.Settings = hostileProfile;

Any slot will do — an object that only ever shows a through-wall marker can fill XRaySettings and leave Settings empty. The outline comes up as soon as the first slot is filled, whether that happens in the same statement, later in the frame, or several frames afterwards. Enabled On Start decides whether it comes up visible.

If you want it added but not yet showing, leave Enabled On Start off and call outline.SetEnabled(true) when the moment arrives. Calling that before a profile is assigned is also fine: it is remembered, and the outline appears when the profile does. OutlineEnabled is the same switch as a property.

Several passes at once

Filling four slots one at a time is four structural changes, and three of the four rebuilds they cause are thrown away by the next assignment. That is invisible when a person fills a slot a minute and very visible in code that spawns a hundred objects. Configure does the lot in one build:

var outline = enemy.AddComponent<KeylineOutline>();

outline.Configure(new KeylineSetup
{
    Main       = hostileProfile,
    XRay       = seeThroughProfile,
    XRayStyle  = KeylineStyle.Neon,   // optional; null keeps the profile's own
    Enabled    = true,                // optional; null leaves Enabled On Start to decide
});

Or, for the common case of profiles and nothing else:

outline.Configure(main: hostileProfile, xray: seeThroughProfile);

A KeylineSetup describes the whole outline, not a patch on one. Every slot is assigned exactly what the setup names, and a slot it leaves null is emptied; a pass draws when its slot has a profile. That is the only reading under which applying the same value twice gives the same result, which is what a setup has to promise. To change one thing, write to that one thing.

A style named in the setup goes through SetStyle, so it follows Instance Override exactly as a direct call would — see 13.2.

13.6 What is cheap and what rebuilds

Three tiers, and it is worth knowing which one a line falls into.

CostsWhat
A material writecolour, opacity, width, glow, gradient offset — everything the shader reads. Fine every frame.
A flag per rendererSetPassEnabled, OutlineEnabled, SetEnabled. Showing and hiding an outline does not rebuild it.
A rebuildputting a different profile in a slot; a style whose shape differs from the last one (Halo, Double, Frost); the expand mode; the feature mask. The generated objects are made again.

Two switches in the second row do rebuild, and only in one combination: turning the visible rim or the X-Ray rim on or off while the other is running. The two share a stencil arrangement, and the extra stamps that carries do not exist until they are built. Every other switch is a flag.

Pooling

Return an object to a pool with OutlineEnabled = false and take it out again with true. Neither destroys anything, so the second is nearly free. Do not remove and re-add the component, and do not clear the slots: both throw away the work the pool exists to reuse.

If a pool holds many outlines dark for long stretches and the memory matters more than the moment one comes back, switch Keep Mirrors While Hidden off under Project Settings ▸ Keyline ▸ Runtime. Hidden outlines are then dismantled and rebuilt on the way back in, which is what every version before 1.2.0 did.

When a write does nothing, and when it does too much

Two mistakes are silent, and both are reported — switch the messages off under Project Settings ▸ Keyline ▸ Runtime ▸ Report API Misuse if the console noise is not wanted. Each is said once per outline and pass.

13.7 One script for a mesh or a sprite

IKeylineOutline is what the two components have in common: the passes, the slots, the three look properties they agree on, the edit scope and Configure. Anything that drives outlines it did not create — a spawner, a save system, a highlight of your own — can hold this instead of a concrete type and work on both.

IKeylineOutline outline = go.GetComponent<KeylineOutline>() as IKeylineOutline
                          ?? go.GetComponent<KeylineOutline2D>();

outline.Configure(new KeylineSetup { Main = profile });
outline.SetPassEnabled(KeylinePassKind.XRay, true);
== null does not work on an interface reference. Destroying a component does not clear the C# references to it; Unity fakes the appearance by overloading == on UnityEngine.Object, and that overload is chosen from the static type of the operand — so through an interface reference it is never chosen at all, a destroyed component reads as present, and the next member access throws. Use the extension provided for it: if (outline.IsAlive()). A loop over other objects' outlines that throws halfway simply stops, and everything after the destroyed entry is silently skipped.

What the interface deliberately leaves out is the look properties past those four. A sprite and a mesh agree on colour, width and opacity; past those the two diverge — hull extrusion has no meaning on a quad, a distance field has none on a mesh — and an interface that promised them would be promising a no-op on one side. Reach the rest through ForkSlot, which hands back the profile itself.

Handing a borrowed pass back

An effect that takes a pass over, writes a temporary look into it and later gives it back has an ordering to respect, and getting it wrong fails quietly. KeylineSlotRestore.ToShared does it correctly:

using FourSsets.Keyline.Recipes;

KeylineSlotRestore.ToShared(outline, KeylinePassKind.MainInterior, myForkedProfile);

It lives beside the recipes, in FourSsets.Keyline.Recipes, because that is where the trap was found — but it is public for anything that borrows a pass, not only for them. Follow it with your own RefreshAppearance(): it restores the style and copies the values back, and putting them on screen is the caller's call.

The style has to be set before the values are copied back — SetStyle returns early when the profile already holds the style being asked for, so it has to run while the slot still holds the effect's style and the difference is visible. A plain value copy raises no event and rebuilds nothing, so a Frost profile restored the other way round comes out still called Frost with its shell mirrors destroyed, drawing a flat band until something else happens to rebuild it.

13.8 What is stable

Public members of KeylineOutline, KeylineOutline2D, KeylineOutlineSettings, IKeylineOutline, KeylineSetup, KeylinePassKind, KeylineStyle and the recipe components are the supported API. Anything added to KeylinePassKind or KeylineStyle goes on the end, so serialized values keep their meaning.

Types whose names say what they are for internally — the pipeline passes, the merge resolver, the stencil pool, the runtime manifest — are public because assembly boundaries require it, not because they are an API. They may change in any release.

Anything that does change in a way a project can notice is listed under Changed, Deprecated or Removed in CHANGELOG.txt, with what to do about it.

14 · Performance

Three things decide what an outline costs: how many hulls the style builds, whether the draws batch, and how many pixels they cover. The first two are measurable and are measured below; the third is fill rate and depends on your screen and your widths.

Soft Edge is extra only when it is actually drawing: the pass is in the pipeline and at least one rim has Edge Softness above 0. That frame pays for an off-screen colour target, a depth copy, a few Kawase blits and a composite. Softness 0, or no pass, costs nothing extra — those rims stay on the camera's transparent path.

14.1 The SRP Batcher

Outlines batch. Measured in the demo's Performance chapter, URP, one Merge Group over the whole grid, style Solid:

Objects with an outlineSRP batches in the transparent queue
2004
4007

Not "4 draw calls" — 4 batches. The two largest at 400 objects hold 166 and 167 draw calls each. Doubling the object count added three batches.

What makes that possible is worth stating plainly, because it decides whether Instance Override is affordable: every object in that grid has its own profile and its own material instance, and they batch anyway. The SRP Batcher keeps material properties in a per-material constant buffer and switches a pointer rather than the pipeline state, so distinct materials on the same shader variant stay in one batch. The batches that do break report a device state change, not a material difference.

What gives the batcher up

Anything that needs a MaterialPropertyBlock, because a block is per-renderer data the batcher cannot fold into its buffer. Four things ask for one, and three of the four only on an object built from several renderers — a single-renderer prop writes the same values onto the shared material and stays batched.

With Cutout on, the same frame debugger shows the outline leaving RenderLoop.DrawSRPBatcher for plain RenderLoop.Draw, one event per draw, and Unity gives the reason itself:

DrawBatch cause reported by Unity
the rimObjects have different materials.
the interior maskSRP: Node is not compatible with SRP batcher

The object's own mesh keeps batching — it is only the outline that drops out. On a fence that is nothing; on a forest it is the first thing to measure. Solid, Neon, Halo and Double place nothing and read no size, so a crowd drawn in those stays batched whatever it is built from.

14.2 Hulls per object

Draw count follows the style's topology, and it is the one cost you can predict without measuring:

StyleHulls per object
Solid, Neon, Sketch, Pattern, Electric, Rainbow, Gradient, Lava, Smoke, Crystal, Water, Fire1
Frost2 — core plus shell
Double3 — two rings and the gap mask
Haloone per layer, 2 to 8 — one on a sprite

14.3 The stencil budget

126 unique pairs per scene, and merging is how you spend fewer: a merge group takes one pair for the whole group, and overlapping outlines cluster automatically. The grid measured above is one group, which is why every batch in it shares a single stencil reference. An Overlay on an object spends a second pair of its own — that cost does not fold into the group.

14.4 Frame time

Measured in a standalone player at 1080p, VSync off, in the demo's Performance chapter — a grid of identical props, one Merge Group, style Solid unless stated. Each figure is the midpoint of a 60-frame average watched until it settled.

The machine these numbers come from
GPUGeForce RTX 4070 Laptop
CPUIntel Core i9-14900HX
Memory / OS32 GB · Windows 11
Unity6000.3 · URP 17.x

What the outline itself costs

Objects outlinedOutlines offOutlines onDifference
641.72 ms1.92 ms+0.19 ms
2001.81 ms2.01 ms+0.19 ms
4001.91 ms2.13 ms+0.23 ms
Read these as an order of magnitude, not as a benchmark. The frame-to-frame spread in each configuration was 0.15–0.44 ms — the same size as the difference being measured. The honest statement is that on this hardware, at this resolution, four hundred outlined objects cost about a fifth of a millisecond and the cost barely moves between 64 and 400. Not that it is exactly 0.19.

That flatness is the batching in 14.1 showing up in the frame time: the work does not scale with the object count until something forces it out of the batcher.

What a style costs

200 objects, same scene:

StyleFrameAgainst Solid
Solid2.00 ms—
Double2.18 ms+0.19 ms
Halo, 5 layers2.30 ms+0.30 ms

Double draws three hulls per object and Halo five, but neither costs three or five times Solid. At this scale the frame is not draw-bound — which is another way of saying the draw-call table in 14.2 predicts the shape of the cost, not the milliseconds.

The Mobile Solid profile

ProfileFrame, 200 objects
Full2.02 ms
Mobile Solid2.04 ms

No measurable difference on a desktop GPU, and that is the expected result. Mobile Solid exists for build size and shader variant count, and for fragment cost on hardware where the fragment stage is the bottleneck. On a 4070 it buys nothing you can see in a frame — pick it for mobile, not for this.

Mixed and the build pause

Building the 200-object grid, wall clock:

ExpandRun 1Run 2
Normal11 ms11 ms
Mixed, first build14 ms11 ms
Mixed, cache warm12 ms11 ms

On these props the sheet analysis does not show up: the spread between two runs of the same configuration is as large as the difference between configurations. Mixed still costs build time rather than frame time — that part is structural, it runs when the hull is built and never again — but on meshes of this size the pause is not something a buyer will notice. On a dense character mesh it will be, which is what baking is for.

14.5 Allocation

Profiled over a steady frame in the Performance chapter, CPU Hierarchy, GC Alloc column: everything allocated in the frame comes from IMGUI — 36.4 KB in the demo HUD's OnGUI plus 4.5 KB in Unity's own GUI plumbing. Outside that subtree the frame allocates nothing.

The outline runtime does not allocate per frame: no garbage from the mirrors, the property blocks or the merge clustering. The 40 KB in the demo is the price of an IMGUI panel, and it leaves with the demo.

14.6 Android

The demo was run on Android under both Vulkan and OpenGLES3, with the Full shader profile, and all ten chapters behave as they do on the desktop. Full is not a desktop-only setting and the mobile-facing features are not cut down there: Cutout, Mixed and all fifteen styles are present.

Mobile Solid is still the faster profile, and the gap widens with the number of outlined objects. On one hero character it is not worth thinking about; on a crowd it is the difference you will measure. The profile decides how many shader variants exist and how much work each fragment does, so the cost it saves is per pixel covered by an outline — which is why object count, outline width and screen resolution all push in the same direction.

The sensible default on mobile is a Mobile Solid Style Profile, moving to an ordinary one only when you reach for something it does not carry — see 15.5 for what that is. On desktop there is no reason not to run Full.

Both kinds are made from Assets ▸ Create ▸ Keyline, and the New button on a pass card copies whichever kind the slot already holds — so a Mobile Solid project stays that way without anybody having to remember. If a bundled profile is deleted, Tools ▸ Keyline ▸ Restore Bundled Style Profiles puts it back, Mobile Solid included.

15 · Troubleshooting

Every entry here is a real constraint of the shipped build rather than a defect: the asset behaves this way on purpose, and the surprise is worth documenting because the behaviour is not guessable.

15.1 Nothing is drawn at all

15.2 Changing one object changed all of them

The profile is a shared asset and the write went straight to it. In the editor the change also survives play mode, because a ScriptableObject edited at runtime is the object on disk.

Write through the component (outline.Color = …), which forks the profile into a copy this object owns, rather than through outline.Settings.color, which does not. See 13.2.

15.3 The cutout outline keeps the old texture

A new texture was written into the same material. The resolved texture is cached against the material's instance id, which has not changed, so nothing signalled that anything did. Call outline.InvalidateCutout() after the swap — 13.4.

15.4 Cutout on a solid model

Experimental — same status as Cutout Overlay. The alpha rim sits on the surface (extrusion is off while Cutout is on). The outer silhouette is a normal hull outline, or a Cutout Overlay (also experimental) on the same pass. Padding that widens a flat card in-plane is skipped on volumes — Cutout still traces alpha.

A flat mesh is still the cleanest card for padding: fences, foliage, decals, a sprite texture on a quad. Single- or double-sided makes no difference; the in-plane grow needs a plane.

15.5 Mixed or a mesh Cutout does nothing on a Mobile Solid profile

Mobile Solid has no vertex path for Mixed — its hull is Normal or Planar and nothing else — and it does not clip a mesh against a texture's alpha. That absence is most of where the saving comes from, and the trade is flat and thin meshes, which Mobile Solid gets wrong.

A sprite is the exception, and it is not a limitation. Every sprite rim is a distance field rather than a hull, so Mobile Solid draws it from the same shared code Full uses and its Rim settings all apply. Solidify — Sheet Thickness, Sheet Lateral, the Thicken axes — works on Mobile Solid too, under Planar.

For the rest, point the pass at an ordinary Style Profile, or accept the limitation deliberately rather than tuning against it.

15.6 Width does nothing on a fill

A fill is the object's own surface, not a hull around it, and the controller forces its width to zero. Colour, opacity and the style are what shape it — 3.2.

15.7 Edge Softness does nothing

The rim stays hard until the Soft Edge pass is in the pipeline. The profile says so beside the Edge Softness slider and offers a button, which is the place it belongs — where the value that needs the pass is being edited. The same switch project-wide is Project Settings ▸ Keyline ▸ Rendering Passes ▸ Soft Edge.

Interior Fill never reads the value, and neither does a rim drawn from a distance field: with Cutout on, or on a sprite, softening is Rim Softness instead and the Edge Softness slider is hidden. If it disappeared from a profile you had already tuned, that is why — switching a profile to Cutout changes which of the two controls its edge. Built-in RP is not supported.

Each profile uses its own world radius. One object's Edge Softness does not change another. The halo stays around the object as the camera moves — it does not spread in screen pixels.

15.8 The whole model is tinted while the wave is elsewhere

The gradient's band is symmetric. Its second colour is not a trailing colour — it is what the object looks like everywhere the front is not. Give it alpha 0 unless the tint is wanted.

15.9 Outlines interfere on a crowd

The stencil pool hands out 126 unique pairs per scene. Past that, further objects share a pair, and objects sharing a pair stop fencing each other off — where they overlap the result is decided by draw order, which reads as flicker.

Merge what belongs together: a group spends one pair however many members it has, and overlapping outlines cluster automatically. See 3.4.

15.10 The SRP Batcher broke

Any MaterialPropertyBlock takes a renderer out of the batcher. Cutout always asks for one; a style told how big the model is, a style that places something in Object or World space, and Planar or Mixed expand from the mesh bounds each ask for one on an object built from several renderers. See 14.1 for the full list and for what stays batched.

All of them are worth the cost where they are needed and worth avoiding on a crowd.

15.11 The demo scene is magenta

Its materials were authored against the other pipeline's Lit shader. The package retargets them on load; if it has not, Tools ▸ Keyline ▸ Fix Demo Materials For This Pipeline. The outline shaders are never involved — they carry a SubShader per pipeline.

15.12 Sheet Solidify does nothing on a character

Topology is never rebuilt for a skinned source: the hull is a second SkinnedMeshRenderer on the same bones, and rebuilding its triangles would break the bone weights. Solidify needs to rebuild topology, so it is skipped there — 10.

15.13 The pixel art outline is soft, or its corners are round

The component reports each of these; see 12.3.

15.14 Width does almost nothing on a pixel art sprite

The profile is in World or Screen mode, where Width is a length. On a sprite at 100 Pixels Per Unit a width of 0.1 is ten texels and the slider crosses several of them at once; on another sprite the same number is three. Set Width Mode to Pixels and the field becomes a whole texel count with a step button either side — 12.3.

15.15 The sprite's outline is thicker on one side than the other

The object's X and Y scale are not equal, so one texel is not square in the world. Make them equal — any value will do, (2, 2, 1) is as good as (1, 1, 1), since it is the ratio between them that stretches a texel and not the size.

On a smooth field there is nothing to do beyond rebuilding: the field is measured on the sprite's own stretched grid and the rim comes out even. Change the scale afterwards and the field is measured for a stretch it is no longer drawn at, which the Inspector says. On a pixel field the scale is the only fix — Stretched sprites.

15.16 A pixel art Pulse flickers instead of breathing

The width of a pixel rim can only change a whole texel at a time, so a rim of six texels swinging by half crosses three boundaries going out and three coming back. Raise Step Fade on the profile: it eases the outline to its faintest at full stretch and back, once per beat, so the steps land where the ring is least visible. If the pulse reads as a flash rather than a swell, lower Brighten — on a stepping rim the colour swing is often doing more of the work than the width is — 12.3.

15.17 What is different on HDRP

Keyline draws the same outlines on both pipelines, but HDRP reaches some of them by a different route, and four consequences of that are worth knowing before they look like faults. Nothing here is a setting to change — they are what HDRP is, and Keyline works within them.

Soft Edge changes how X-Ray is drawn, project-wide. HDRP leaves an outline shader two usable stencil bits, which is not enough to fence an X-Ray family against a soft rim on the camera. So when the Soft Edge pass is installed, Keyline draws the whole X-Ray family — rims and Interior Fill — inside that pass instead of on the camera, for every object, at any Edge Softness. Switching Soft Edge on or off therefore changes the route X-Ray takes. It does not change what it looks like, and if nothing in the scene has softness above zero the pass stands aside and the ordinary route is used. URP fences with the camera stencil and only moves soft rims.

Outlines composite before post-processing. The Soft Edge pass runs at BeforePostProcess, so on HDRP an outline that goes through it is post-processed with the rest of the frame: temporal anti-aliasing, depth of field, motion blur and any upscaler act on it. A thin rim under TAA can shimmer, and depth of field will blur an X-Ray rim belonging to an object that is itself in focus. On URP the rims are ordinary transparent geometry and take the same route as everything else in the scene.

Outline colour is not exposure-compensated. Keyline writes the colour you chose, and HDRP's colour buffer is scaled by the camera's exposure. Under Fixed exposure that is stable and the colour is the colour. Under Automatic exposure the scene re-meters as the camera moves and the outline does not follow, so a rim that reads correctly in a bright room will read differently in a dark one. Pin the exposure, or pick the colour in the lighting the outline will mostly be seen in.

Outlines do not appear in ray-traced effects. The shaders carry no ray tracing passes, so an outline is drawn in the direct view only — never inside a ray-traced reflection, ray-traced global illumination, or the path tracer. Project Settings ▸ Keyline says so when ray tracing is enabled in the HDRP asset.

And one that is fatal rather than awkward: both of Keyline's HDRP passes are Custom Passes. If Custom Pass support is switched off in the HDRP Asset, or the Custom Pass frame setting is off on a camera, they cannot run — Soft Edge and X-Ray Layer Filtering are then missing with the switch still reading On. The settings page reports the first; the second is per camera and worth checking there if one view has outlines and another does not. Ordinary outlines are unaffected either way.

16 · Reference

16.1 Menu paths

MenuEntry
Add ComponentKeyline ▸ Keyline Outline 3D
Add ComponentKeyline ▸ Keyline Outline 2D
Add ComponentKeyline ▸ Draw Config — 3D only, one per scene
Add ComponentKeyline ▸ Merge Group — meshes and sprites alike
Add ComponentKeyline ▸ Keyline Occluder 2D
Add ComponentKeyline ▸ Keyline Occluder Layers 2D
Add ComponentKeyline ▸ Recipes ▸ … — thirteen entries
Project SettingsKeyline — project-wide switches, 16.4
Assets ▸ CreateKeyline ▸ Style Profile
Assets ▸ CreateKeyline ▸ Mobile Solid Style Profile — the same asset drawn by the smaller shader
ToolsKeyline ▸ Bake All Outlines In Scene
ToolsKeyline ▸ Settings — opens the page above
ToolsKeyline ▸ Refresh Pipeline Defines
ToolsKeyline ▸ Restore Bundled Style Profiles
ToolsKeyline ▸ Fix Demo Materials For This Pipeline — only while the demo folder is present
ToolsKeyline ▸ Add Draw Config

16.2 Enumerations

All in namespace FourSsets.Keyline.

KeylineStyle

Solid · Neon · Halo · Sketch · Pattern · Double · Electric · Rainbow · Gradient · Frost · Lava · Smoke · Crystal · Water · Fire — see 6.

KeylinePassKind

Primarythe Main rim
XRaythe rim drawn where the object is hidden
MainInteriorthe fill
XRayInteriorthe fill drawn where the object is hidden
MainCutoutOverlaythe Cutout Overlay drawn with the Main rim — mesh only
XRayCutoutOverlaythe Cutout Overlay drawn with the X-Ray rim — mesh only

A sprite answers false for the last two and ignores an attempt to switch them: it has four mirrors and no overlay pass.

KeylineWidthMode

Worldmetres; the outline keeps its proportion to the object
ConstantScreenpixels; the same thickness at any distance
Pixelswhole texels of the sprite's texture; 2D pixel art

KeylineSdfFieldMode

SmoothEuclidean distance, sub-texel edge; round corners
PixelArt8Chebyshev — a diagonal step costs one; outlines diagonals
PixelArt4Manhattan — a diagonal costs two; thinner, no diagonals

KeylineExpandMode

Normalalong the surface normal — right for volume
Planarin the plane of the face — right for sheets
Mixedper face, whichever of the two applies

KeylineExpandCenterMode

MeshBoundsin-plane expansion radiates from the mesh's bounds centre
ObjectPivotfrom the transform's own origin
Customfrom a point given in object space

KeylineFaceCull

Frontdraw front faces — the classic inverted hull
Backdraw back faces
Bothdraw both — for single-sided sheets seen from either side

KeylineStyleSpace

ObjectSpacethe pattern is painted on the mesh
WorldSpacethe object moves through a fixed field
ScreenSpacethe pattern stays put on screen
UVSpacethe pattern follows the unwrap

KeylineGradientShape

Lineara ramp along Angle, in the object's own space
Radiala sphere spreading from a world point
Planara plane travelling along a world axis

KeylineGradientRadiusMode

RelativeRadius is multiplied by the object's own size — one setting fits every model
AbsoluteRadius is metres — what a front crossing several objects needs

KeylineMixedDetectMode

Thicknessone ray per face; cheaper, coarser
MultiRaya cone of rays per face; slower to bake, far fewer misses

KeylineRainbowMotion

Verticalbands travel up the object
Circularbands rotate around a centre

KeylineBlendMode

Defaultthe style decides, as it always has
Alphaordinary transparency — the only mode where Opacity covers
Additivelight; the background always shows through
Screenlight that approaches white instead of clipping at it
Multiplyink; darkens and tints rather than covering
Subtractthe colour taken out of the frame
Darken · Lightenwhichever is darker or brighter, channel by channel

See 5.5.

KeylineShaderProfile

Fullevery style, cutout, Mixed expand
MobileSolidSolid only, Normal or Planar expand, no mesh cutout — fewer variants and less fragment work. Called Light up to 1.1; the value is stored as a number, so renaming it changed no asset.

KeylineShaderFeatures

A [Flags] mask recording which shader variants a build needs: None, Cutout, StyleNeon, StyleHalo, StyleSketch, StylePattern, StyleDouble, StyleElectric, StyleRainbow, StyleGradient, StyleFrost, StyleLava, StyleSmoke, StyleCrystal, StyleWater, StyleFire, PlanarExpand, AllStyles, All. AllStyles is every style bit; All is that plus Cutout and PlanarExpand.

The mask does not decide what may render — the style does. It records what the build has to keep, and its style bits are derived from the Style field automatically. That separation exists because the old behaviour let a profile sit on a style whose bit was off and render silently as Solid.

16.3 Shader properties

The outline material is driven by the controller, and almost every property on it is written every time the appearance is applied. Setting them yourself is possible and pointless — the next refresh overwrites them.

The two shaders are 4ssets/Keyline/Outline (Full) and 4ssets/Keyline/OutlineMobileSolid (Mobile Solid). Which one draws is decided by the profile's type: an ordinary Style Profile is drawn by Full, a Mobile Solid Style Profile by the other. Both components read that and swap the material with it, which is why pointing a pass at the other kind of profile rebuilds rather than reconfigures.

Everything says Mobile Solid now — the shader, its material, the bundled Profile_MobileSolid asset, the C# type KeylineMobileSolidOutlineSettings and the KeylineShaderProfile.MobileSolid value. It was called Light up to 1.1, which read as a lighting feature: the one thing it has nothing to do with.

It used to be a dropdown on one profile, and that was the wrong shape for it. Mobile Solid is a different program with a different set of values it can honour — one style, no cutout branch on a mesh — so a profile switched to it went on saying Halo in every field the page still showed and drew a plain line. A type cannot do that.

It works on sprites too. It draws Solid and nothing else, with no style branches to step through, and it measures its rim against the same distance field the Full shader uses — from the same shared code, so the two cannot come out different shapes. On a phone that is the largest single saving available to a 2D project.

If you fork the shader, keep the property names: the controller addresses them by Shader.PropertyToID and a renamed property silently stops being written rather than failing loudly. Assign your copy in the component's Outline Material slot.

16.4 Project Settings

The project-wide switches live under Project Settings ▸ Keyline, or Tools ▸ Keyline ▸ Settings, which opens the same page. They come in two kinds, and the difference matters more than it looks.

Rendering Passes — Soft Edge and X-Ray Layer Filtering — are the only two switches on the page that install something. Ticking one puts Keyline's pass into your render pipeline; unticking it takes the pass out again. Both are project-wide in both pipelines: nothing is added to any scene, and nothing has to be repeated per scene.

Keyline asks about them once, on the first reload after import, and never again. Say yes and both go in; say no and nothing is written and nothing is asked a second time — the switches are here whenever you want them. Neither is on until somebody says so, because on URP turning one on writes a renderer feature into your own Universal renderer assets, and a package has no business editing those before it has asked. Without them Edge Softness has no effect and every X-Ray Sees Through mask is ignored; both say so where they are set, and the rest of Keyline works exactly as it does with them.

Where the switch is kept differs by pipeline, and it is worth knowing which you have.

Because both are one project-wide answer, the Soft Edge button on Keyline Outline 3D and this checkbox are two views of one thing rather than two settings, and cannot disagree.

Each switch says whether it is actually working, in a line under it, in Edit Mode and in Play Mode. On is not the same as working, and the two ways they part company are pipeline-specific, so you are only told about the one that can happen to you:

The switches are read-only while the game is running — installing edits assets, and doing that mid-play gives you a change the running game has already gone past. The status line stays live.

Turning a pass off changes nothing you have authored. Soft Edge off draws rims hard, X-Ray Layer Filtering off ignores every X-Ray Sees Through mask and gives you the ordinary X-Ray — no component is edited and no value is lost, so ticking it back on restores everything exactly.

Reset To Defaults covers them too, and asks first when it would uninstall a pass. Every other switch on the page is a preference; these two change what your project renders, and a reset that did it unannounced would be doing more than the button says.

Four more are Editor preferences. Two are on out of the box and two are off, and the split is not about how useful each one is. The two that are on are the ones whose off state is the confusing one — an outline that ignores a child just added to it, an outline that vanishes with the mesh asset behind it. The two that are off change how the Scene view answers a click and what a build contains: both are worth having, neither is worth starting unasked.

And two are neither. The Runtime section holds the only settings on this page that reach a player build — see below.

The Keyline page under Project Settings
Two switches over the render pipeline, three for working in the Editor, one that decides what a build contains, and two the build carries into the game.
Rendering passDefaultWhat it does
X-Ray Layer FilteringOff Lets each outline choose which layers its X-Ray shows through, using the X-Ray Sees Through mask on the component — 3.5. Costs nothing until a mask is actually narrowed: with every outline seeing through everything there is nothing to test, so no buffer is allocated and no geometry is drawn.
Soft EdgeOff Draws Main and X-Ray rims into a separate buffer and blurs them, so Edge Softness and world-space halos work — 4.4. The same switch as the buttons on Keyline Outline 3D.
SettingDefaultWhat it does
Mirrors Not ClickableOff Takes Keyline's generated mirror objects out of Scene view picking. A mirror sits on top of the object it outlines and is usually a little larger, so a click lands on the mirror rather than on what you meant to select — and dragging it moves the outline off the object until the next rebuild throws the edit away. Takes effect on the next rebuild.
Rebuild On Hierarchy ChangeOn Rebuilds an outline in the Editor when the renderers under it change, so a child added to an outlined object gets an outline without pressing anything. The hierarchy event only prompts a comparison — the rebuild happens when the set actually differs, not every time something moves in the scene, so an unrelated change costs one walk of a cached list. On by default: off, the child appears with no outline and nothing says why. Turn it off if you would rather rebuild only when you press the button. Editor only, and it covers every outline in the project; for play mode and builds there is a per-component switch instead, below. The two never act at the same moment.
Rebuild On Baked Mesh DeletedOn Rebuilds outlines whose saved bake has just been deleted from the project. A mirror is handed the baked mesh directly, so deleting the asset leaves it holding nothing and the outline disappears — with no message, and it stays gone until something unrelated forces a rebuild. On by default: this one never runs unless something has already broken, and the alternative is a silent failure whose cure is unguessable — the outline comes back when you toggle the profile, which teaches the wrong lesson about what fixed it.
Strip Unused Shader VariantsOff Leaves out the shader variants for styles no profile in the project uses: smaller builds, shorter shader compiles. The risk is that a style chosen at run time — assigned from code, or on a profile authored later — has no variant to draw with and falls back to Solid. A plain outline where a Neon one was asked for is harder to notice than nothing at all, so turn it on deliberately and check the build.

Runtime

Two switches that change what the game does rather than what the Editor does, and the only ones on this page that reach a player build. They travel in a small manifest Keyline writes at build time and deletes afterwards, so a project that never opens this page runs on the defaults below and gets no extra file in its Assets folder.

SettingDefaultWhat it does
Keep Mirrors While HiddenOn An outline switched off leaves its generated objects in place with their renderers disabled, so switching it back on is a flag per renderer and costs nothing. Off, they are destroyed and the next show builds them again — less memory held by objects nobody is looking at, paid for at the moment somebody looks. On by default because the hide-and-show cycle is the common one: a hover, a selection, a ping. Turn it off when a scene hides many outlines at once and leaves them hidden. See 13.6.
Report API MisuseOn Reports the two writes that fail silently: a look property written while the pass's slot is empty, and one written onto a shared project asset with Instance Override off. Each is said once per outline and pass. The first is reported in the Editor and in development builds; the second in the Editor only, because telling a project asset apart from one you made yourself means asking the asset database. A release build never checks — neither test is compiled into one. See 13.6.
Where they are stored. ProjectSettings/KeylineSettings.asset, outside Assets. Reimporting Keyline cannot overwrite them, and version control carries them, so a team and its build machine agree — which matters most for Strip Unused Shader Variants, since it decides what ships.

Auto Rebuild In Play Mode, per component

The switch above covers the Editor and nothing else — it is compiled out of a build entirely. Both outline components carry their own Auto Rebuild In Play Mode under Setup for the other half, off by default — and the reason the defaults differ is that the two watch very different things. In the Editor a hierarchy change is a rare, human-speed event, so watching for one costs nothing. In a build it is a standing cost paid to catch a case most projects never have: code that spawns or destroys part of an outlined object without telling anything.

It listens for Unity's own child-list message rather than polling, so an untouched object pays nothing per frame. What that message reports is the object's direct children: a renderer added deeper down is not reported by Unity and needs Rebuild(), which is one call and is already to hand in the code doing the restructuring.

17 · Licence and credits

Keyline is licensed under License.txt in the package root — the Unity Asset Store EULA governs, and that file states in plain words what it means here.

The example art in the package — pattern textures, cutout textures, demo models and the demo character — is by Kenney and is in the public domain under CC0 1.0 Universal. Every component, its path and the full licence text are listed in Third-Party Notices.txt, also in the package root. CC0 asks for no attribution; the credit here is given because the work deserves it.

Keyline is made by 4ssets, part of UnityCraft.org. Questions, bugs and requests go to the same address.