API Reference
Every public type and member in the FourSsets.Keyline namespace, from the source's
own documentation. Start with IKeylineOutline: it is the
same on a mesh and on a sprite, and it covers what game code usually needs. The
Scripting chapter of the manual walks through whole recipes.
I want to…
| Task | Use |
|---|---|
| Show or hide an outline | OutlineEnabled · SetEnabled |
| Switch one pass on or off (X-Ray, fills) | SetPassEnabled · XRayOutlineEnabled · MainFillEnabled |
| Change colour, width or opacity | SetPassColor · SetPassWidth · SetPassOpacity |
| Change the style | SetStyle · SetPassStyleProfile |
| Change one object without touching the shared asset | InstanceOverride · ForkSlot · RevertSharedPassStyleProfiles |
| Find the outline under the mouse or a raycast | TryGetFrom |
| Add an outline at runtime | GetOrAdd · Configure |
| Close gaps along hard edges and UV seams | JoinCoincidentVertices · SetJoinCoincidentVertices |
| Choose how hard corners are built | CornerTopology · CornerSharpLimit |
| Tune how flat parts are found and thickened | SheetSolidifyEnabled · SheetThicknessCm · MiterLimit |
| Change many outlines at once | SetOutlineEnabled · SetPassColor |
| Group several changes into one rebuild | EditScope |
| Outline only some parts of a model | SetPassEnabled · ClearPassOverrides |
| React when an outline changes | OutlineEnabledChanged · PassStyleProfileChanged · Rebuilt |
| Refresh after changing a profile or the hierarchy | RefreshAppearance · Rebuild |
| Make a profile at runtime | Create · CloneForRuntime |
| Check a stored reference is still alive | IsAlive |
| Check whether a feature is switched on in the project | Allows |
| Move smoothly to another profile | BlendTo · IsBlending |
| Draw the outline on its object's layer (cameras, render features) | MirrorLayer · SyncLayers |
| Hide the outline when its renderer is switched off | HideWithRenderer |
| Outline a LOD Group from one level | OutlineFromLod |
| Fade outlines out with distance | FadeStart · SetReference |
Start here
The interface every outline answers to, and the helpers around it.
interface IKeylineOutline
FourSsets.Keyline · Runtime/IKeylineOutline.cs
An outline on a mesh (KeylineOutline) or on a sprite (KeylineOutline2D), driven through one API.
More about IKeylineOutline
Covers what both components share: the master switch, the four hull passes, their profiles, colour, width, opacity, style and pulse. Everything else is on the concrete component, or on the profile returned by ForkSlot.
Get one with KeylineOutlineRef.TryGetFrom or KeylineOutlineRef.GetOrAdd. Check a stored reference with KeylineOutlineRef.IsAlive, not == null.
// Works the same on a mesh (Keyline Outline 3D) and a sprite (Keyline Outline 2D).
IKeylineOutline outline = KeylineOutlineRef.GetOrAdd(gameObject);
outline.OutlineEnabled = true; // master switch
outline.SetPassEnabled(KeylinePassKind.XRayOutline, true); // one pass
outline.SetPassColor(KeylinePassKind.MainOutline, Color.yellow); // look
outline.Pulse = true;Properties
Runtime look writes go to a private copy of the profile instead of the shared asset.
More
Turning it off does not drop copies already made; call RevertSharedPassStyleProfiles.
The master switch: whether the outline is drawn at all.
More
Off keeps profiles and pass switches, so on restores the same outline. Cheap when the value does not change. Set before the component is enabled, it applies on enable and wins over Enabled On Start.
Main Outline switch: the rim drawn where the object is visible.
X-Ray Outline switch: the rim drawn where the object is hidden.
Main Fill switch: the tint over the visible body.
X-Ray Fill switch: the tint over the hidden body.
bool XRayEnabled { get; set; }propertyobsoleteRenamed to XRayOutlineEnabled.
Renamed to XRayOutlineEnabled.
bool InteriorEnabled { get; set; }propertyobsoleteRenamed to MainFillEnabled.
Renamed to MainFillEnabled.
bool XRayInteriorEnabled { get; set; }propertyobsoleteRenamed to XRayFillEnabled.
Renamed to XRayFillEnabled.
Color Color { get; set; }propertyRim colour of the pass being edited (Main Outline outside an edit scope).
float Width { get; set; }propertyRim width of the pass being edited, in the profile's Width Mode units.
float Opacity { get; set; }propertyOpacity of the pass being edited, 0 to 1.
bool Pulse { get; set; }propertyWhether the pass being edited pulses.
bool IsEditScopeOpen { get; }propertyWhether an edit scope is open.
bool IsActiveAndEnabled { get; }propertyWhether the component is enabled on an active object.
Methods
Whether a pass's switch is on. Reads back what was set.
More
An empty slot still answers true; use IsPassDrawing for what is on screen.
Whether a pass is on screen: component live, master switch on, pass switch on, slot filled and its Project Feature on.
Switch a pass on or off. Its profile stays in the slot.
The profile a pass draws with: the private copy if there is one, else the asset.
Put a profile in a pass's slot. Null empties the slot. Rebuilds.
More
Assigning the profile already there does nothing. Prefer Configure when setting several slots.
Whether a pass wears styleProfile. With includeForks, a private copy made from it counts too.
More
Null asks whether the slot is empty.
In play mode, give a pass a private copy of its profile and return it. Outside play mode, returns the asset itself.
More
Write fields on the result, then call RefreshAppearance. Null when the slot is empty.
Whether a pass is currently drawing with a private copy.
Move a pass to another profile over time. Numbers and colours slide when the two profiles draw alike; otherwise the outline fades out, changes over and fades in. The shared assets are not edited, and at the end the profile itself is in the slot. Any other write to the pass ends the blend where it is.
More
Outside play mode, with zero seconds, an empty slot or a null profile, the profile is assigned at once.
outline.BlendTo(KeylinePassKind.MainOutline, hoverProfile, 0.25f);
True while BlendTo is moving this pass.
Jump a running blend to its end.
Stop a running blend where it is; the pass keeps the in-between look.
Set one pass's rim colour. Material write only.
Set one pass's rim width, in the profile's Width Mode units.
Set one pass's opacity, 0 to 1.
Set the style of the pass being edited (Main Outline outside an edit scope).
More
A style switched off in Project Settings ▸ Keyline ▸ Features draws as Solid.
Start a batch of writes to one pass. Structural changes wait for the matching end.
End the batch opened by BeginPassStyleProfileEdit and apply it.
A batch of writes to one pass, closed by using.
More
using (outline.EditScope(KeylinePassKind.XRayOutline))
{
outline.SetStyle(KeylineStyle.Neon);
outline.Width = 0.03f;
}
Set up every hull pass from one description, with one rebuild.
More
A slot the description leaves empty is emptied. See KeylineSetup.
World bounds the outline covers. False when there is nothing.
void Rebuild()methodRebuild the generated objects from the current profiles. Does nothing while disabled.
void RefreshAppearance()methodRe-apply the profiles to the materials without rebuilding.
KeylineStyleProfile PassSettings(KeylinePassKind pass)methodobsoleteRenamed to PassStyleProfile.
Renamed to PassStyleProfile.
Renamed to SetPassStyleProfile.
Renamed to SetPassStyleProfile.
void BeginSettingsEdit(KeylinePassKind pass)methodobsoleteRenamed to BeginPassStyleProfileEdit.
Renamed to BeginPassStyleProfileEdit.
void EndSettingsEdit()methodobsoleteRenamed to EndPassStyleProfileEdit.
Renamed to EndPassStyleProfileEdit.
Events
Raised when OutlineEnabled actually changes.
Raised after a slot is given a different profile. Not raised for private copies.
Raised after a Rebuild finishes.
class KeylineOutlineRef
FourSsets.Keyline · Runtime/IKeylineOutline.cs
Finding, adding and checking outlines without naming the concrete component.
// Pick an outline from a raycast, whatever the collider sits on.
if (Physics.Raycast(ray, out RaycastHit hit)
&& KeylineOutlineRef.TryGetFrom(hit.collider, out IKeylineOutline outline))
{
outline.OutlineEnabled = true;
}
// A stored reference: IsAlive, not == null, also catches a destroyed component.
if (_selected.IsAlive()) { _selected.OutlineEnabled = false; }Methods
Whether this outline still exists. Use it instead of == null on an interface reference, which does not see destroyed components.
Whether this highlight target still exists. See the outline overload.
The outline that draws hit: on the object or a parent, resolved to the outline that controls the figure when the one found was generated or adopted.
More
No allocation. For raycast and pointer selection.
if (Physics.Raycast(ray, out RaycastHit hit)
&& KeylineOutlineRef.TryGetFrom(hit.collider, out IKeylineOutline outline))
{
outline.OutlineEnabled = true;
}
The outline on gameObject, adding one if there is none: Keyline Outline 2D on a sprite, Keyline Outline 3D otherwise. Null for a null object.
More
A new component starts with empty slots; give it profiles with IKeylineOutline.Configure.
IKeylineOutline outline = KeylineOutlineRef.GetOrAdd(enemy);
outline.Configure(new KeylineSetup { MainOutline = hostileProfile, Enabled = true });
class KeylineOutlines
FourSsets.Keyline · Runtime/IKeylineOutline.cs
The same change applied to many outlines at once, skipping destroyed and null entries.
More about KeylineOutlines
No allocation. Each call costs what the single-outline call costs, per outline.
KeylineOutlines.SetOutlineEnabled(squad, true);
KeylineOutlines.SetPassStyleProfile(squad, KeylinePassKind.MainOutline, selectedProfile);
Methods
Set the master switch on every outline in the list.
Switch one pass on or off on every outline in the list.
Put one profile in one pass's slot on every outline in the list.
Set one pass's rim colour on every outline in the list.
struct KeylineEditScope : IDisposable
FourSsets.Keyline · Runtime/KeylineEditScope.cs
An open burst of writes to one pass, closed by leaving the block.
More about KeylineEditScope
What it is for. Every structural change an outline is asked for — a profile into a slot, a style whose shape differs from the last one — rebuilds the generated objects, because a half-applied change is a visible wrong frame. Four changes in a row are therefore four rebuilds, and three of them are thrown away by the next. A scope says "these belong together": nothing rebuilds until it closes, and then once.
using (outline.EditScope(KeylinePassKind.XRayOutline))
{
outline.SetPassStyleProfile(KeylinePassKind.XRayOutline, profile);
outline.SetStyle(KeylineStyle.Neon);
outline.Width = 0.03f;
}
It also answers a question the API cannot otherwise ask. Every member that
changes a pass names it in the call — except SetStyle, which takes a style and
nothing else. The scope is how that one is told which pass it means. Outside a scope it
writes to the visible rim.
A struct, and disposed by value. using on a struct that implements
IDisposable calls Dispose without boxing, so an inner-loop scope allocates
nothing. It is not reusable and should never be stored — take it, use it, leave the
block.
Exceptions close it. That is the reason to prefer this over the
BeginPassStyleProfileEdit / EndPassStyleProfileEdit pair by hand: a throw between the two
leaves the outline with a scope open for ever, and every later change silently deferred.
// Several writes to one pass, one rebuild at the end.
using (outline.EditScope(KeylinePassKind.MainOutline))
{
outline.SetPassColor(KeylinePassKind.MainOutline, Color.red);
outline.SetPassWidth(KeylinePassKind.MainOutline, 0.05f);
}Methods
public void Dispose()methodClose the burst. Anything the writes deferred happens here, once.
struct KeylineSetup
FourSsets.Keyline · Runtime/KeylineSetup.cs
A whole outline described in one value, so that setting one up costs one build.
More about KeylineSetup
The problem it solves. Assigning four slots one at a time is four structural changes, and each one rebuilds the generated objects against a state the next assignment is about to replace. Three of the four builds are thrown away. That is invisible in the inspector, where a human assigns one slot a minute, and very visible in code that spawns a hundred objects.
var outline = go.AddComponent<KeylineOutline>();
outline.Configure(new KeylineSetup
{
MainOutline = mainProfile,
XRayOutline = xrayProfile,
XRayOutlineStyle = KeylineStyle.Neon,
});
It describes the whole outline, not a patch on one. Every slot is assigned exactly what the setup names, and a slot left null is emptied — so configuring an outline that was already set up replaces it rather than adding to it. That is the only reading under which the same value applied twice gives the same result, which is what a setup has to promise. To change one thing, write to that one thing.
A pass draws when its slot has a profile. There is no separate switch here,
because a pass with nothing in its slot has nothing to draw and one with a profile was given
it for a reason. Switch a pass off afterwards with SetPassEnabled, which keeps the
profile and is cheap.
Styles are optional, and they write into the profile. A style named here goes
through SetStyle, so it follows Instance Override exactly as a direct call would: at
runtime with the switch on, the slot forks first and the project asset is untouched; with it
off, the asset is edited. Leave a style null to keep whatever the profile already says,
which is the usual case.
// Configure an outline added at runtime in one call.
IKeylineOutline outline = KeylineOutlineRef.GetOrAdd(enemy);
outline.Configure(new KeylineSetup
{
MainOutline = mainProfile,
XRayOutline = xrayProfile,
Enabled = true,
});Properties
public KeylineStyleProfile Main { get; set; }propertyobsoleteRenamed to MainOutline.
Renamed to MainOutline.
public KeylineStyleProfile XRay { get; set; }propertyobsoleteRenamed to XRayOutline.
Renamed to XRayOutline.
public KeylineStyleProfile Interior { get; set; }propertyobsoleteRenamed to MainFill.
Renamed to MainFill.
public KeylineStyleProfile XRayInterior { get; set; }propertyobsoleteRenamed to XRayFill.
Renamed to XRayFill.
public KeylineStyle? MainStyle { get; set; }propertyobsoleteRenamed to MainOutlineStyle.
Renamed to MainOutlineStyle.
public KeylineStyle? XRayStyle { get; set; }propertyobsoleteRenamed to XRayOutlineStyle.
Renamed to XRayOutlineStyle.
public KeylineStyle? InteriorStyle { get; set; }propertyobsoleteRenamed to MainFillStyle.
Renamed to MainFillStyle.
public KeylineStyle? XRayInteriorStyle { get; set; }propertyobsoleteRenamed to XRayFillStyle.
Renamed to XRayFillStyle.
Methods
The profile this setup names for a pass.
The style this setup names for a pass, or null when it names none.
Renamed to StyleProfileFor.
Put this setup into an outline, inside one edit scope so it costs one build.
More
Shared by both components rather than written twice: every call it makes is on IKeylineOutline, and two copies of an ordering this delicate is how the two would drift apart. The master switch is settled by the caller before this runs — see either Configure — because asking for it afterwards would build a second time.
Fields
Profile for the visible rim, or null to leave that slot empty.
Profile for the rim that shows through occluders.
Profile for the fill over the visible part.
Profile for the fill over the hidden part.
Style for the visible rim, or null to keep the profile's own.
Style for the X-Ray rim, or null to keep the profile's own.
Style for the fill over the visible part, or null to keep the profile's own.
Style for the fill over the hidden part, or null to keep the profile's own.
public bool? EnabledfieldWhether the outline draws at all once configured, or null to leave that as it is.
More
Null on a component just added means Enabled On Start decides, which is the answer almost everything wants. Say false to build the outline now and show it later — the mirrors are made either way, so the reveal costs nothing.
class KeylinePassKinds
FourSsets.Keyline · Runtime/KeylineOutlineController.Passes.cs
Questions about a KeylinePassKind that every caller was asking.
Methods
True for the two Cutout Overlay passes, which only a mesh outline has.
Whether an overlay kind is the X-Ray one. Meaningless for the other four.
public static string Label(KeylinePassKind kind)methodstaticThe pass's name as the inspector writes it on its card.
More
For messages a person reads. The enum member is not that name — "Primary" is on no card, and a warning that says to check the Primary slot sends the reader looking for something that is not in front of them. Both inspectors title these cards the same way, which is what this repeats; the sprite component has no overlay pass, so those two answers only ever appear on a mesh.
Whether Project Settings ▸ Keyline ▸ Features lets a pass draw on this kind of outline.
Whether a value is one of the defined passes.
Fields
public const int HullPassCount = 4fieldstaticThe four passes every Keyline component has. The two overlays are extra.
public const int Count = 6fieldstaticEvery pass a mesh outline can address.
Components
What you add to a GameObject.
class KeylineOutline : KeylineOutlineController
FourSsets.Keyline · Runtime/KeylineOutline.cs
Outline on an object. Assign KeylineStyleProfile per feature slot.
Every member is declared on its base class, KeylineOutlineController.
class KeylineOutlineController : IKeylineBlendHost
FourSsets.Keyline · Runtime/KeylineOutlineController.Blend.cs, Runtime/KeylineOutlineController.ChildOutlines.cs, Runtime/KeylineOutlineController.Cutout.cs …
Which layers this outline's Main passes are allowed to draw in front of.
More about KeylineOutlineController
The problem it exists for. A Main rim is a hull around the object, and a hull reaches past the surface it hugs. Where it reaches into the floor the object stands on, the floor is nearer than the rim and the depth test cuts it — correct by the letter of the test and wrong by everything else, because a crate on the ground is not behind the ground. With Edge Softness the cut is worse than a missing pixel: the blur is built from a rim that has already been sliced, and the slice becomes a hard line across the glow.
What it does. Listing the floor's layer here tells the Main Outline and Main Fill passes that the floor never hides them. The hardware depth test is handed over to the shader for those two passes — the same occluder buffer and the same test X-Ray Sees Through already uses, asked with this mask instead — so everything else still hides them exactly as before, and the glow is cut against the nearest <em>blocking</em> occluder rather than against whatever the camera's depth happens to hold.
The default is Nothing, and that is today's picture. The X-Ray mask defaults to Everything because an X-Ray rim starts out visible through everything; a Main rim starts out visible through nothing, so the same meaning — "these layers do not hide me" — has the opposite default. With nothing listed the feature is not compiled in, the depth test stays where the hardware does it, and the outline is the one that shipped in 1.2.0.
It needs the pass, and it waits for it. With no pass filling the buffer every read says "nothing occludes anywhere". An X-Ray outline can fall back to that safely — it only ever clips further — but a Main outline that had handed its depth test over would draw in front of the whole scene. So the test only moves once a pass is actually recording; until then the mask is stored and the outline draws as it always has. Pass In Pipeline under Project Settings ▸ Keyline ▸ Features ▸ Layer Filtering is the switch, and the component offers a button once a layer is listed.
var outline = GetComponent<KeylineOutline>();
outline.SetEnabled(true);
outline.MainOutlineEnabled = true;
outline.XRayOutlineEnabled = true;
// Only this object, not every object sharing the profile asset.
outline.InstanceOverride = true;
outline.SetPassColor(KeylinePassKind.MainOutline, Color.cyan);
// After changing the hierarchy under it (meshes added or removed):
outline.Rebuild();
// Hull geometry: bevelled corners, and split vertices joined within 2 mm.
outline.CornerTopology = KeylineCornerTopology.Rounded;
outline.SetJoinCoincidentVertices(true, 2f);Properties
True when this controller was created by a parent outline rather than by hand.
True when a parent outline claimed this one through AdoptChild rather than by growing a generated component.
The outline that currently claims this one — generated owner or adoptive parent — or null when this outline is independent.
The child outlines this one created. Empty unless it is splitting them.
Renamed to OwnedChildren.
Renamed to OwnedChildren, the sprite component's name.
Outlines claimed through AdoptChild. Empty until something asks.
Unity's own answer, surfaced on the interface.
Whether writes fork the slot instead of landing on the shared asset. Turning it off does not un-fork slots already forked — call RevertSharedPassStyleProfiles.
Which layer the mirrors are put on. MatchRenderer matches each mirror to its renderer's layer and rendering layers; Fixed keeps them all on layer 0 as 1.2.0 did.
In play mode, check every frame whether a renderer changed layer and move its mirrors. Off by default; SyncLayers does the same once, on request.
Stop drawing a renderer's outline while the renderer is switched off or its object is inactive. Renderers hidden by the Reveal recipe keep theirs.
The LOD level of a LOD Group whose renderers the outline is built from. Clamped to the group's levels. Changing it rebuilds.
Follow the source material's alpha cutout instead of the polygon edge — grass cards, foliage, decals.
Which faces draw this pass (not the interior stencil mask).
The look this profile draws.
Which program draws this pass: Full, or the cheap Solid-only Mobile Solid one.
More
Read-only, because it is a property of the profile asset's type rather than a value on it — a KeylineMobileSolidStyleProfile is Mobile Solid and an ordinary profile is Full. To change it, point the pass at a different profile. The setter that used to be here would now be written straight back out by validation, which is a worse answer than not offering it.
Full-shader feature mask (ignored on Mobile Solid profile). Solid style is always allowed.
Main colour of the outline.
Second colour of the styles that use one.
How far the outline reaches out from the surface.
Overall transparency of the whole outline, on top of the colours' own alpha.
<inheritdoc/>
World keeps the outline in proportion to the object: it thins out with distance and goes away with the thing it is on.
public KeylineExpandMode ExpandMode { get; set; }propertyobsoleteObsolete.
Always KeylineExpandMode.Mixed: since 1.3.0 every hull is built one way, solid parts grown along their normals and sheets in their own plane. Setting it does nothing.
Give detected sheets a shell, so a card still has an outline seen edge-on. Off leaves in-plane growth only.
Sheet detection: Thickness (one ray) or MultiRay (a cone of rays).
Sheet detection: neighbour flatness, read only when a thickness ray misses the face. Clamped to 0.5–0.995.
Sheet detection, MultiRay: rays per face, from 1 (the normal only) to 4.
Sheet detection, MultiRay: cone half-angle in degrees, 5–30. Read only when more than one ray is cast.
Sheet detection: smallest face island that may count as a sheet.
Smallest connected piece of the finished outline, in triangles, that is kept. Applies where the outline builds its own copy of the mesh. Changing it rebuilds.
Sheet detection: thickness in centimetres. Thinner than this counts as a sheet. Clamped to 0.01–1.
Sheet detection: gap in centimetres under which a border still counts as a hard edge of the volume. A flat piece farther off the surface is separate. Clamped to 0.01–2.
public KeylineExpandCenterMode ExpandCenterMode { get; set; }propertyobsoleteObsolete.
Always KeylineExpandCenterMode.MeshBounds. The centre Planar grew from; nothing reads it since 1.3.0, and setting it does nothing.
public Vector3 ExpandCenterCustom { get; set; }propertyobsoleteObsolete.
Always zero. The custom centre Planar grew from; nothing reads it since 1.3.0, and setting it does nothing.
With Smooth Normals on: how far a hard edge may push its outline out to keep the same thickness as a curved one. 1 (default) adds nothing; 1.45 brings square edges to the full Width. Clamped to 1–8.
public float PlanarWidthScale { get; set; }propertyobsoleteObsolete.
Always 1. Planar's stretch along the object's local X; nothing reads it since 1.3.0, and setting it does nothing.
public float PlanarHeightScale { get; set; }propertyobsoleteObsolete.
Always 1. Planar's stretch along the object's local Y; nothing reads it since 1.3.0, and setting it does nothing.
Sheet Solidify with one number for the shell: thickness and in-plane spread together, × Width. Reading it under Separate returns Thickness. Setting it writes Volume, Thickness and Lateral. 0–2.
Sheet Solidify: size the shell with Thickness and Lateral separately rather than the single Volume.
Sheet Solidify: how far the shell is pushed out to each side of the sheet, × Width (Separate). 0–2.
Sheet Solidify: shell growth within the sheet's plane, × Width (Separate). 0–2.
Sheet Solidify: push the shell towards one side of local X (+ = +X, − = −X). −2–2.
Sheet Solidify: push the shell towards one side of local Y (+ = +Y, − = −Y). −2–2.
Effective Solidify thickness (±N). Uniform uses Sheet Volume.
Effective Solidify in-plane scale. Uniform uses Sheet Volume.
True when Solidify topology should be active (enabled + either axis > 0).
Brightness multiplier for the styles that emit.
Neon / Frost / Water: how quickly the view-facing rim fades.
Sketch / Electric: how far the line is pushed around by the noise.
Sketch / Electric: how fast the noise re-rolls.
Frost: reach of the outer halo, in the same units as Width.
Frost: opacity of the outer halo relative to the core band.
Pattern: greyscale mask tiled along the outline.
Pattern: tiling of the mask.
Pattern: scroll speed of the mask along X and Y, in tiles per second.
Pattern: contrast applied to the mask before the threshold — sharpens soft artwork into a crisp stencil.
Pattern: mask level that separates visible from clipped.
Double: thickness of the inner ring, in the same units as Width.
Double: clear space between the two rings.
Double: thickness of the outer ring, drawn in the Secondary colour.
Rainbow: how tightly the spectrum repeats along the ramp.
Rainbow: how fast the bands travel.
Rainbow: colour purity, 1 full spectrum, 0 greyscale bands.
Rainbow: Vertical runs the bands down the screen; Circular orbits them around the object's centre.
Circular Rainbow: object-local X of orbit center (0 = pivot).
Circular Rainbow: object-local Y of orbit center (0 = pivot).
Rainbow, Vertical: sweep direction in degrees. 0 is the vertical axis, the same bands the style had before this existed.
Rainbow, Vertical: how far the sweep leans out of the style space's XY plane, in degrees. 0 stays in the plane. Meshes in Object or World space only.
Frost: density and brightness of the shimmering ice sparks.
Frost: how large a patch of plain ice is left where the surface faces the camera, 0 to 1. Zero shows the strata everywhere, pinwheel included.
Gradient: direction of the sweep in degrees. 0 sweeps bottom to top.
Gradient: how far the sweep leans out of the style space's XY plane, -90 to 90 degrees. Zero keeps it in the plane, where GradientAngle alone sets it. Read by Linear in Object and World space and by the Radial stretch axis.
Gradient: slides the ramp along its own direction, -1 to 1. The parameter a gauge drives: with a transparent colour at one end of the ramp and an opaque one at the other, this is where the boundary between them sits.
Gradient: width of the blend between the two colours. 0 is a hard edge.
Gradient: Linear ramps along the angle, Radial ramps outwards from GradientCenter.
Gradient, Radial only: the world point the ramp spreads from. World rather than local on purpose. A wave that several objects have to render in step needs one origin they all agree on, and a local point would give each of them their own.
Gradient, Radial only: how far from the centre the ramp spans, in metres.
Gradient: 0 is a ramp, above zero a travelling band of this half-width.
Gradient, Radial only: how far the ramp reaches along GradientAngle relative to across it. 1 is a sphere.
Gradient, Radial only: how far the ramp reaches along the Angle axis.
Halo: how many concentric layers the glow is built from. Structural, unlike the other Halo values: each layer is a mirror renderer, so changing the count builds or tears down geometry rather than re-tinting it.
Coordinate space the style's pattern, noise or gradient lives in. Object space rides with the object, world space stays put as it moves, screen space stays put as the camera moves, UV space follows the mesh's own layout.
Pulse: beats per second.
Pulse: how deep the swing goes, as a fraction of the resting width.
How the outline composites over the frame. Default lets the style decide, which is what every profile did before blend modes existed.
Paint each pixel once however many times the hull covers it — the overlap fence. Honoured only by the single-band styles; the layered ones (Halo, Frost, Double) are built out of stacking and ignore it, exactly as the inspector explains. 3D only: a sprite's outline is one quad and covers no pixel twice.
Screen-space fade on the active rim pass, in pixels. 0 is a hard edge. Interior Fill ignores the value. Needs the Soft Edge pass in the pipeline.
Cutout rim: how far the edge fades out, in screen pixels.
Cutout rim: how far the outline runs under the artwork, in screen pixels.
Lava: how far the crust has broken open, 0 solid to 1 all glow.
Lava: size of the crust cells.
Lava: how fast the molten core moves under the crust.
Lava: brightness of the hot core inside the cracks.
Lava: direction the flow runs, in degrees.
Lava: how much the flow curls instead of sliding straight.
Lava: how fast the glow breathes, in beats per second.
Lava: how deeply the pulse swings, as a fraction of the heat.
Smoke: how much of the cloud survives.
Smoke: size of the billows.
Smoke: how fast the cloud drifts.
Smoke: direction of the drift within the style space's XY plane, in degrees.
Smoke: how far the drift leans out of that plane, in degrees. Zero stays in it.
Smoke: how much the billows curl as they travel.
Smoke: how far along the drift the cloud reaches, as a fraction of the shape.
Smoke: how raggedly the cloud ends, once Height has decided where.
Crystal: size of the facets.
Crystal: direction of the light the facets catch, in degrees.
Crystal: how fast the light source orbits the stone.
Crystal: how much fire the stone has — how many facets catch the light.
Crystal: ceiling on how far a lit facet washes out towards white.
Crystal: how visible the cut edges between facets are.
Crystal: how evenly sized the facets are.
Crystal: depth shading of the facets.
Crystal: how far neighbouring facets split between the two colours.
Water: how fast the ripples travel.
Water: direction of the flow, in degrees.
Water: size of the ripples.
Water: brightness of the caustic crests.
Water: how much the ripples curl rather than scrolling straight.
Water: view-facing sheen on the crests.
Fire: how fast the tongues rise.
Fire: direction the flame rises within the style space's XY plane, in degrees.
Fire: how far the rise leans out of that plane, in degrees. Zero stays in it.
Fire: how far up the object the flame reaches.
Fire: how stretched the tongues are along the rise.
Fire: size of the tongues.
Fire: how much the tongues fold and curl as they rise.
Fire: brightness of the root, past the colour into white-hot.
Fire: how much the whole flame gutters over time.
Layers that do not hide this outline's Main Outline and Main Fill.
Judge the Main passes at the object's own depth rather than at the hull's.
More
See the field's tooltip for what that means on screen. Switching it writes the same state a mask edit does, because it moves the same depth test.
How far past the object's own volume the contact test reaches, in metres.
Draw the children inside this outline's silhouette rather than beside it.
Share a silhouette with whatever this overlaps, if that one agrees too.
How many renderers were dropped for their type on the last collect.
One of the dropped renderers, for a message that can name something.
Persisted bakes currently assigned to this controller (read-only).
Whether the outline draws at all — the master switch above every pass.
More
The property form of SetEnabled, and the name the sprite component has always used for the same thing. Both spellings stay: a verb reads better at a call site that is doing something, a property at one that is copying a state from somewhere else, and code written against either component now compiles against the other.
Off does not clear anything. The profiles stay in their slots and the per-pass switches keep their positions, so turning it back on restores the outline that was there — see KeylineRuntimeFlags.KeepMirrorsWhileHidden for what that costs.
Main Outline switch: the rim drawn where the object is visible.
Optional X-Ray Outline (through-walls) style profile. Use XRayOutlineEnabled to toggle without clearing.
X-Ray Outline switch: the rim drawn where the object is hidden.
Optional Main Fill style profile. Use MainFillEnabled to toggle without clearing.
Main Fill switch: the tint over the visible body.
Optional X-Ray Fill style profile. Independent of XRayOutlineStyleProfile. Use XRayFillEnabled to toggle.
X-Ray Fill switch: the tint over the hidden body.
public bool XRayEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to XRayOutlineEnabled.
Renamed to XRayOutlineEnabled.
public bool InteriorEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to MainFillEnabled.
Renamed to MainFillEnabled.
public bool XRayInteriorEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to XRayFillEnabled.
Renamed to XRayFillEnabled.
Keep the soft X-Ray rim out of this object's silhouette, so X-Ray Fill reads cleanly underneath it.
More
What it fixes. A hard rim is already fenced out of the silhouette: the mask stamps the body's claim and the rim tests against it. Edge Softness then blurs that rim in screen space, and a blur has no stencil — it spreads the rim's colour inward, over the area X-Ray Fill occupies. The fill ends up with a glowing border painted on top of it.
How. The pass already punches the object's body out of the blurred rim so the glow does not land on its albedo, and it punches only where the body wins depth — hidden fragments are left glowing on purpose, because that is the X-Ray look. This extends the punch to the whole silhouette, and the area it adds is exactly the hidden part of the body.
There is no Main twin, and that is the conclusion of two wrong turns worth recording. The first version hung this on the body entry, which made it one answer per object and let the X-Ray switch move Main with it. The second gave Main its own switch — and that one had nothing to do, because the ordinary punch already covers the entire visible silhouette, which is where both the Main rim and Main Fill live. Only the X-Ray pair meets on hidden fragments, and only because the punch spares them deliberately. A control that is inert in every reachable configuration is worse than no control: it sends people looking for the breakage.
The per-group plumbing stayed, and has to. The flag travels on the rim entries and is part of the group key, so switching it on for X-Ray does not drag a Main rim of equal Edge Softness into the same cut. That was the bug the second version fixed, and removing the Main switch does not remove the need for it.
Why not a draw-order change, which is the obvious reading of the request. The fill cannot simply be moved above the rim in the queue. It sits between its family's two stencil stamps and both write the same value: what separates the visible body from the hidden one is the moment the fill draws, not any value it could test for. Moving it after both stamps makes NotEqual match nowhere and the fill disappears; the spare stencil bit that would let it tell the two stamps apart does not exist. Clipping the rim instead reaches the same picture without touching that order at all.
Whether this object's visible outline stays off its own silhouette where something passes through the body.
More
Two stencil-only stamps of the silhouette per source renderer, one either side of the rim. Switching it rebuilds because that is whether those stamps exist — there is no material knob to refresh.
Keep the X-Ray rim's soft glow outside the object's silhouette instead of letting it wash across the body.
More
For an X-Ray Interior fill underneath that has to read cleanly. Needs Edge Softness above zero on the X-Ray profile, and the Soft Edge pass installed.
Optional second draw of Main Outline. Hull (Cutout off) extrudes outside; Cutout traces alpha on the surface. Pair with MainOutlineStyleProfile.
When true and MainCutoutOverlayStyleProfile is set, the Main overlay draws.
Optional second draw of XRay Outline. Hull or Cutout, paired with XRayOutlineStyleProfile.
When true and XRayCutoutOverlayStyleProfile is set, the XRay overlay draws.
public KeylineStyleProfile Settings { get; set; }propertyobsoleteRenamed to MainOutlineStyleProfile.
Renamed to MainOutlineStyleProfile in 1.3.0.
public KeylineStyleProfile XRaySettings { get; set; }propertyobsoleteRenamed to XRayOutlineStyleProfile.
Renamed to XRayOutlineStyleProfile in 1.3.0.
public KeylineStyleProfile InteriorSettings { get; set; }propertyobsoleteRenamed to MainFillStyleProfile.
Renamed to MainFillStyleProfile in 1.3.0.
public KeylineStyleProfile XRayInteriorSettings { get; set; }propertyobsoleteRenamed to XRayFillStyleProfile.
Renamed to XRayFillStyleProfile in 1.3.0.
Renamed to MainCutoutOverlayStyleProfile.
Renamed to MainCutoutOverlayStyleProfile in 1.3.0.
Renamed to XRayCutoutOverlayStyleProfile.
Renamed to XRayCutoutOverlayStyleProfile in 1.3.0.
public bool HasSettings { get; }propertyobsoleteRenamed to HasMainOutlineStyleProfile.
Renamed to HasMainOutlineStyleProfile in 1.3.0.
Whether writes are currently being held back by an open scope.
True when any rim slot on this object asks for a soft edge. Interior is ignored. Used by the inspector to warn when the pipeline pass is missing.
Layers this outline's X-Ray may be seen through.
True when the Main Outline slot holds a style profile.
Main Outline style profile. Null leaves Main Outline off; other slots stay independent.
More
Writing through this reference bypasses Instance Override. The property
hands back the ScriptableObject itself, so controller.MainOutlineStyleProfile.color = red
edits the shared profile and turns every object using it red — and in the editor the
asset stays modified after play mode ends. The controller cannot intercept that; it
never sees the write.
At runtime, change the look through the controller's own properties
(Color, Width, Style and the rest), which
fork the slot first. For a batch of changes, or for a field with no property, take the
object's private copy with ForkSlot and write to that:
KeylineStyleProfile mine = controller.ForkSlot(KeylinePassKind.MainOutline);
mine.color = Color.red;
mine.glowIntensity = 4f;
controller.RefreshAppearance();
Assigning a profile to this property is always safe — that swaps which asset the slot points at rather than editing one.
True when the outline system is on for this object. Reads the primary pass directly, so it is correct no matter which pass is momentarily active.
The material every mirror of this outline is instanced from, or null for the one Keyline loads out of its own Resources folder.
More
Set it only to substitute a material of your own built on the Keyline shader. It is a template: each pass instances it and writes its own values, so the asset assigned here is never modified.
True while the active PassState is the XRay (or XRay Interior) rim/fill.
True while the active PassState is an interior fill pass.
Active stencil ref for this pass (primary or occluded half).
The widest reach of any rim this component currently draws, in metres.
More
The widest, not the sum. Every pass is extruded from the same surface, so they overlap rather than stack — Main and X-Ray at the same width reach exactly as far as one of them. Adding them would push the figure out by a rim that is not there.
Interior passes are not asked. They fill the silhouette and are drawn at width 0, so they never reach past the body at all.
Draw the outline when the scene starts.
More
Off, the outline is built but not shown, and OutlineEnabled reveals it when the moment arrives. Read before the first profile lands, so an object given its profile from code still honours it.
Average the normals of vertices that share a position before extruding the hull.
More
What closes the gaps at a hard edge: a cube's corner has three normals pointing three ways, and extruding along each of them pulls the rim apart. Costs an analysis of the source mesh on the first build, cached afterwards. Changing it rebuilds.
Whether hard corners of the hull get a bevel. Default Rounded.
More
Rounded adds the corner triangles, so the band stays even around a hard corner. Simple leaves the baked vertex where it is, so the corner stays a single point, with its reach set by CornerSharpLimit. Needs Smooth Normals or Sheet Solidify. A mesh with blend shapes gets no bevel; its corners stay single points. Changing it rebuilds.
Rounded, Sheet Solidify shells only. Sharpest corner, in degrees, that still gets a round. A tighter corner stays sharp. Default 0. Changing it rebuilds.
Rounded, Sheet Solidify shells only. Widest corner, in degrees, that still gets a round. A wider corner stays sharp. Default 120. Changing it rebuilds.
public float CornerRoundAngle { get; set; }propertyobsoleteUse CornerRoundAngleTo. Corners round between CornerRoundAngleFrom and CornerRoundAngleTo.
Rounded only. Upper end of the round-angle range. Prefer CornerRoundAngleFrom and CornerRoundAngleTo.
Rounded, Sheet Solidify shells only. 0 keeps a corner sharp, 1 pulls it into a full circular arc. Default 1. Changing it rebuilds.
Rounded, Sheet Solidify shells only. A corner is left sharp when the arc between its edges would be shorter than this, in the same units as the outline width. Default 0.015. Changing it rebuilds.
Rounded, Sheet Solidify shells only. A corner stays sharp when the planes around it fold by more than this many degrees, either way. 180 rounds those corners too. Default 15. Changing it rebuilds.
Simple only. How far a sharp corner may reach, as a multiple of the outline width, from 1 to 4. 1 keeps it short and the band narrows there. Default 1. Changing it rebuilds.
Also join vertices that only nearly meet — within JoinDistanceMm of each other — in the outline's copy of the mesh. On by default.
More
A mesh split along a hard edge or a UV seam keeps two vertices at one point, and each one extruded along its own faces would open a gap in the hull there. Vertices on the same point are therefore always joined, whatever this says. What this adds is the near miss: two vertices a fraction of a millimetre apart, as an export or a careless snap leaves them, which open the outline just the same. Texture coordinates are kept per face, so Cutout and UV Style Space still see the seam. The source mesh is never changed.
Applies wherever the outline's copy bakes its own normals: Smooth Normals, or Sheet Solidify. The same in the Editor, in Play Mode and in a build. A mesh that is not Read/Write enabled needs Bake for Runtime. Off leaves near misses apart, for a gap the mesh keeps on purpose. Changing this rebuilds.
How close two vertices must be, in millimetres of mesh space, before the outline's copy joins them. Default 1, range 0 to MaxJoinDistanceMm. 0 joins only vertices on the same point, as Join Coincident Vertices off does. Changing it rebuilds.
Extra surface around a cutout's artwork for the rim to be drawn on, in metres.
More
Changing it rebuilds: the padding is baked into the outline's copy of the mesh, not applied per frame, so there is nothing for a cheap refresh to pick up.
How far the distance field beside each cutout texture describes the shape, in texels.
More
The ceiling on rim width. Setting it rebuilds the fields, which is editor-only work — a build ships the fields it was given.
Whether padding is worked out from the field rather than set by hand.
Outline every renderer under this object, not only the ones on it.
More
The usual answer for a prop or a character built from several meshes. Use the ignore list and LayerMask to leave particular renderers out. Changing it rebuilds.
Transforms skipped when IncludeChildren is on (the transform and all descendants).
Only renderers on these layers receive an outline. Changing it rebuilds.
Draw the outline in the Scene view while the game is not running.
More
Editor only, and nothing to do with EnabledOnStart, which answers the same question for play mode. Off leaves the component configured and the scene clear.
True when mirrors should be shown right now (Play Mode or Edit Mode preview).
Rewrite the style when a part of this figure moves inside it.
More
See the tooltip on the field for what it is for and what it costs. Turning it on does not refresh by itself: the values are right until something moves, and the first tick has nothing to compare against.
Methods
public void BlendTo(KeylinePassKind pass, KeylineStyleProfile styleProfile, float seconds, bool unscaledTime = false)methodIKeylineOutlineMove a pass to styleProfile over seconds.
More
Colours, widths and the other numbers slide when the two profiles draw with the same mirrors; otherwise the outline fades out, changes over and fades in. The shared assets are never edited: the in-between look lives in this object's own copy, and at the end styleProfile itself is in the slot. Calling it again during a blend carries on from the current look. Any other write to the pass ends the blend where it is.
Outside play mode, with zero seconds, an empty slot or a null profile, the profile is assigned at once, as SetPassStyleProfile does.
| Parameter | |
|---|---|
pass | The pass to change. |
styleProfile | The profile to end on. |
seconds | How long the blend takes. |
unscaledTime | Ignore Time.timeScale, so the blend runs during a pause. |
True while BlendTo is moving this pass.
Jump a running blend to its end: the target profile in the slot, now.
Stop a running blend where it is. The pass keeps the in-between look on its own copy.
Treat child as a child of this outline.
More
The next Rebuild includes it the way a hierarchy child is included — in the hull when children are folded, or with this outline's profiles pushed onto it when they are split. The child's own component is not destroyed. It does not have to sit under this transform: a sword picked up from the floor is the case this is for.
Calling it again with the same child does nothing. A child already claimed by another outline is released there first. Nested calls are part of the same figure: a sword that has already adopted a gem still joins the character that adopts the sword, and the gem comes with it.
Stop treating child as a child of this outline.
More
The child's component stays where it is and draws for itself again. This outline rebuilds without it.
Become a child of parent. Same as parent.AdoptChild(this).
Stop being an adopted child. Same as OutlineParent.ReleaseChild(this) when this outline was claimed through AdoptChild.
The field for a texture, or null when none has been generated.
More
A linear walk, deliberately. The list holds one entry per distinct texture on one object — two or three in practice, never enough for a dictionary to be worth the allocation or the trouble of keeping it in step with a serialized list.
Supply the field for a texture this object will draw with.
More
For textures assigned while the game runs. The editor fills this in by itself for anything a material already points at, but it cannot anticipate a texture chosen at run time — and neither can a build, which includes what something references and nothing else. Whatever hands out those textures is what has to hand out their fields.
| Parameter | |
|---|---|
source | The cutout texture this field was built from. |
field | The distance field, or null to forget the pairing. |
Supply the field and say what range it was built with.
More
For a field whose name does not carry the number — one built by hand, or renamed. Get it wrong and every distance in the field is scaled by that ratio, so the rim comes out the wrong width while looking perfectly well formed.
| Parameter | |
|---|---|
source | The cutout texture this field was built from. |
field | The distance field, or null to forget the pairing. |
spread | Range the field was built with, in texels of the source. |
Supply the field, its range and the threshold it was baked at.
More
All three travel together because all three have to agree with the material for the rim to land on the artwork's edge — and none of them can be recovered from the pixels if they do not.
| Parameter | |
|---|---|
source | The cutout texture this field was built from. |
field | The distance field, or null to forget the pairing. |
spread | Range the field was built with, in texels of the source. |
cutoff | Threshold the field was baked at. |
The source meshes this component needs to read and cannot, or an empty list.
More
Why this exists. A mesh without Read/Write is the quietest failure Keyline has. Sheets, Solidify, Cutout Padding and smoothed normals all silently fall back to the raw hull, so the outline is drawn — it is simply the wrong one, the mesh pushed along its authored normals. There is a console warning, but a warning that scrolls past once, hours before anyone looks at the object, is not how somebody finds out why a setting they are dragging does nothing.
It answers with the same rules the bake uses — the same source list, the same needs-a-bake test, the same persisted-bake check — because a notice that fires where the bake would not have is worse than no notice: it sends people to fix something that was never wrong.
| Parameter | |
|---|---|
results | Filled with one entry per offending renderer. |
need | One sentence naming what the mesh was needed for. |
True when something is wrong and the caller should say so.
Source meshes whose Read/Write can be switched back off, because a runtime bake has already been taken and Keyline will never read them again.
More
Why offer it at all. Read/Write keeps a second copy of every mesh in system memory for the whole life of the application. It is the price of the analysis, and once the analysis is in an asset the price is being paid for nothing — on a scene full of outlined props that is real memory, in a build, forever.
Cutout Padding is excluded, and not out of caution. Padding is re-planned from the source mesh on every build, before the baked mesh is even looked for, and the planner refuses outright on an unreadable mesh. Offering to switch it off there would break the padding quietly, which is the exact failure this pair of diagnostics exists to end.
Safe to act on because it is self-correcting: the bake is keyed to the settings it was taken under, so changing sheet detection, Solidify or Smooth Normals afterwards stops it matching, and CollectUnreadableSourceMeshes says so on the next repaint.
Dump the real GPU-facing state of every pass. Reading the code cannot show which values actually reached the material, so this reports them directly.
True when this slot is running on a clone rather than on the shared asset.
Fork a slot now and return the clone the object owns. Useful when you are about to write a burst of properties and want the clone to exist before the first of them, or when you want to hand the instance profile to something else — a tween, a gauge — without going through the look properties one at a time.
Fork every assigned slot in one go.
public void SyncLayers()methodPut every mirror on the layer MirrorLayer asks for, now. Call it after changing an outlined object's layer from code when FollowLayerChanges is off. Cheap: one comparison per mirror, and a write only where something changed.
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
Retarget stored width fields (px ↔ meters) so visual thickness stays similar when switching KeylineWidthMode. Uses the active camera distance / FOV (same relationship as ConstantScreen in the shader). Falls back to 1px ≈ 1cm.
Re-apply the merge rules after the fields behind them were written directly.
More
The Inspector does not go through the property setters. PropertyField edits the serialized field, so everything the setters do — telling the descendants their answer changed, asking clustering to re-run — was skipped, and both switches looked stuck in their on state. This is the same work, asked for once after the write.
What this outline resolved to, and the inputs it was resolved from.
More
Written because three guesses in a row were wrong. Every way a merge can go wrong looks identical in the Scene view — two things share a silhouette, or fail to — and the rules are indistinguishable by eye. This prints the answer instead of inviting another guess.
Bake config hash used by mesh cache and persisted Bake for Runtime assets.
True when any outlined renderer (Include Children / layer) is a SkinnedMeshRenderer.
True when this mesh is used by a SkinnedMeshRenderer in the current outline collect set.
The one place that decides which renderer types Keyline can outline. It used to be a term buried in the filter above, next to the layer mask and the mirror marker, with no comment of its own — so from the code it was impossible to tell whether sprites were excluded on purpose or as the leftover of an abandoned attempt. Product decisions and housekeeping should not share a condition.
Number of renderers that will receive an outline with the current setup.
Mirror renderers currently alive across all passes (rough draw-call cost).
True when the active pass would run a CPU mesh bake for this source kind. Only these meshes are worth persisting via Bake for Runtime.
True when a persisted bake exists but no longer matches the current settings, so the runtime would silently fall back to CPU analysis (or skip the effect on non-readable meshes). The Inspector surfaces this as a "re-bake" warning.
True when a persisted bake was assigned but its asset is no longer there.
More
Distinct from being stale. A stale bake still exists and simply no longer matches the settings; the lookup skips it and the CPU path takes over on the next rebuild, which is a graceful fall. A missing one has been deleted out from under a mirror that is still holding it, and a mirror whose mesh has been destroyed draws nothing at all — the outline vanishes with no message and stays gone until something unrelated forces a rebuild.
Nothing in the runtime can notice that on its own: deleting a project asset is an editor event and this component is never asked about it. So the editor watches for deletions and asks this — see KeylineBakedMeshWatcher.
Drop all persisted bakes (the runtime falls back to CPU analysis).
Replace persisted outline mesh entries (Bake for Runtime).
Unique source meshes used by the current Include Children / Layer Mask setup.
Create the outline's prepared copy of a mesh (no cache / persisted lookup). Caller owns the mesh.
Switch a pass on or off for the parts named here.
More
Each entry in parts names a block: the transform itself, everything under it, and everything it has adopted, all the way up the chain. That last part is what makes this usable on a figure assembled with AdoptChild, where a block need not sit under the transform that claims it — naming the block at the break names the whole section that comes away with it.
Naming parts turns the pass on, then narrows it. Whenever this call can ask for something to be drawn — enabled true, or exclusive true, where the parts not named are the ones drawn — the pass's switch is set on first, so one call is the whole answer whatever the pass was doing before. Only a call that hides the named parts and says nothing about the rest leaves the switch where it was: there is nothing in it to show.
The two-argument SetPassEnabled is the whole-figure switch. It forgets everything said here about this pass, so a highlight ends with it whichever way it is thrown.
Merge Child Outlines on or off, the same answer. On, the parts are mirrors in this outline's hull. Off, they are drawn by outlines of their own — generated for the children, or adopted ones this outline is not folding in — and every one of those reads this list and takes this outline's pass switches, so naming a part works the same either way and costs no rebuild in either. Called on one of those part outlines, the call is passed up to the outline that speaks for the figure.
Overlapping parts: the one named last wins, which is how every other setter on this component behaves.
| Parameter | |
|---|---|
exclusive | What happens to the parts this call does not name. False, the default: nothing. They keep whatever they had. Use this when several rules are being combined and each one speaks only for itself — a base that is silenced for good, say, must not come back the next time something else is lit. True: they are given the opposite of |
Forget what this pass was told about individual parts.
Forget what every pass was told about individual parts.
Whether this pass draws on this part — the pass's own switch and what it was told about that part, together, which is the question the picture answers.
Point a Cutout Overlay slot at the same style profile as Main Outline (As Main).
public void SetPassStyleProfile(KeylinePassKind pass, KeylineStyleProfile styleProfile)methodIKeylineOutlinePut a profile in a pass's slot. Null empties the slot. Assigning the profile already there does nothing; a different one rebuilds that pass.
Point a feature slot at the same style profile as Main Outline (As Main).
Whether a pass is switched on, addressed by kind rather than by one of four properties. Anything generic over the passes — a flash that can paint either fill, an editor tool, a save system — otherwise has to carry the same four-way switch, and each copy is a place to forget the fourth case.
<inheritdoc/>
Switch a pass on or off for the whole figure, addressed by kind.
More
Whatever the pass was told about individual parts is forgotten first, in both directions: on means every part, and off leaves nothing to come back the next time the pass is switched on. To light some parts only, name them with SetPassEnabled.
The profile assigned to a pass, addressed by kind. Null when the slot is empty.
public bool IsPassStyleProfile(KeylinePassKind pass, KeylineStyleProfile styleProfile, bool includeForks = false)methodIKeylineOutlineWhether this pass is wearing styleProfile.
More
What counts as wearing it is the one question worth asking here, and includeForks is how you answer it. Writing a look value at runtime with Instance Override on forks the slot: the object stops pointing at the project asset and starts pointing at a private copy of it. From then on "is this the Hostile profile" has two honest answers — no, it is a copy; and yes, it is a copy of Hostile.
False, the default: the slot holds this exact object. A fork is a different object and does not match its original. Use it when the identity is the point — a save system recording exactly what is assigned, or a check that a slot has not been forked behind your back.
True: the slot holds this object, or a fork made from it. Use it when the intent is the point — "is this enemy still showing the Hostile look, whatever I have tinted on top of it". A pulse, a flash, a colour tween all fork the slot, and without this every one of them would make the object stop being recognisably Hostile.
Null asks whether the slot is empty.
Renamed to PassStyleProfile.
Renamed to PassStyleProfile in 1.3.0.
public void SetSettings(KeylinePassKind pass, KeylineStyleProfile settings)methodIKeylineOutlineobsoleteRenamed to SetPassStyleProfile.
Renamed to SetPassStyleProfile in 1.3.0.
Renamed to AssignFeatureStyleProfileFromMain.
Renamed to AssignFeatureStyleProfileFromMain in 1.3.0.
Renamed to BeginPassStyleProfileEdit.
Renamed to BeginPassStyleProfileEdit in 1.3.0.
public void EndSettingsEdit()methodIKeylineOutlineobsoleteRenamed to EndPassStyleProfileEdit.
Renamed to EndPassStyleProfileEdit in 1.3.0.
Say that a burst of writes to kind is about to begin.
More
Two jobs. It names the pass, which is how SetStyle — the one member that takes no pass — is told which one it means. And it holds back structural work: nothing rebuilds until the matching EndPassStyleProfileEdit, so a burst of changes costs one rebuild rather than one each.
Prefer EditScope, which pairs the two for you and survives an exception. A Begin whose End is skipped leaves the outline deferring everything for ever.
Close a burst opened by BeginPassStyleProfileEdit, and do the work it held back.
More
Restores whichever pass was active before that Begin — the primary rim in the ordinary case, and the enclosing scope's pass when scopes are nested. Unpaired, it falls back to the primary rim, which is what it always did.
Open a burst of writes to one pass, closed by leaving the using block.
More
The pair above with the ending guaranteed. See KeylineEditScope.
Set this outline up from one description, in a single build.
More
The four slots assigned one at a time are four structural changes, and three of
the four builds they cause are thrown away by the next assignment. This does the lot
inside one edit scope. See KeylineSetup for what the description means —
in particular that it describes the whole outline, so a slot it does not name is
emptied.
var outline = go.AddComponent<KeylineOutline>();
outline.Configure(new KeylineSetup { MainOutline = mainProfile, XRayOutline = xrayProfile });
The common case of Configure — profiles and nothing else.
More
Written for named arguments: Configure(main: rim, xray: seeThrough) reads as what it does and does not ask the caller to name slots they are leaving empty.
Activate a Cutout Overlay so look writes and Bake for Runtime hash that draw. Always pair with EndPassStyleProfileEdit.
Re-read the cutout texture from the source materials. The resolved texture is cached against the source material's instance id, so 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. So the one case that needs telling is exactly this one: mat.SetTexture("_BaseMap", damaged) at runtime, on a material an outline is reading. Call this after it and the outline follows.
The keyword a style needs, or null for Solid — which is the absence of them all.
World AABB of outline sources, for the overlap rule. False when there is nothing.
More
Measured once per frame, and this is the reason. Every caller of this is a per-frame or slower one — the X-Ray draw-order sort runs it for every outline on every camera, and the merge resolver runs it for every outline ten times a second — and each call walked the source list and read Renderer.bounds from every renderer in it. Each of those reads crosses into native code, so the cost was the number of outlines times the number of renderers under each, paid again for every camera in the frame. On a prop built from a dozen meshes that is the whole of it.
Why a frame is the right granularity. The answer does not depend on which camera is asking — it is a world AABB — so two cameras in one frame must get the same number, and a third asking between them cannot legitimately get a different one. Rendering happens after the frame's movement has been applied, so a value measured in the frame it is used in is the value the old code would have produced.
A structural change does not wait for the next frame: adding or removing a renderer invalidates this along with the source list, because a set that has changed makes the measurement wrong rather than merely stale.
Called when merge-group membership or group enable state changes.
Set JoinCoincidentVertices and, optionally, JoinDistanceMm together, with one rebuild.
More
| Parameter | |
|---|---|
join | True joins near misses too. False joins only vertices on one point. |
distanceMm | How close two vertices must be, in millimetres, from 0 to |
var outline = enemy.AddComponent<KeylineOutline>();
outline.SetJoinCoincidentVertices(true, 2f); // join within 2 mm
Replace the ignore list and rebuild mirrors when the outline is built.
Build or tear down edit-mode preview mirrors.
Set the master switch. Same as setting OutlineEnabled.
More
Does nothing when the value is unchanged. On a disabled component the value is stored and applied when it is enabled. Set in play mode, it takes precedence over Enabled On Start.
public void Toggle()methodFlip the master switch. See OutlineEnabled.
Rim colour of the pass being edited. The property form is Color.
<inheritdoc/>
Point Outline Material at the package material for the current profile's program, and rebuild the mirrors.
More
Not needed to switch programs: which program draws is the profile asset's type, each pass's mirrors are made from the package material for its own profile when they are built, and assigning a profile of the other type rebuilds them. What this adds is the Outline Material field itself following the profile, and a rebuild now. A custom material is left in place, with a warning.
Which optional shader branches this profile is allowed to compile.
More
A build-size control rather than a look one: the mask is what the variant stripper reads. Narrowing it can take a style away, so the style falls back to Solid rather than naming one the shader cannot draw. Rebuilds when it changes what exists.
True when the Full-shader feature bit is enabled (always false on Mobile Solid profile).
Solid is always allowed; other styles require their Feature Mask bit (Full only).
Inspector helper after SerializedProperty edits to ActiveSettings.shaderFeatures.
Throw the generated objects away and make them again from the current profiles.
More
Rarely needed from code: every member that changes something structural asks for this by itself. Reach for it after writing to a profile directly — through ForkSlot — in a way that changes what has to exist rather than what it looks like. For the latter, RefreshAppearance is the cheap one.
Immediate, including inside an edit scope: it names an action rather than a consequence, and code that asks for a rebuild is asking for one now.
Not re-entrant, and the Editor will try. A rebuild tears the generated objects down and makes them again, and in the Editor tearing down means DestroyImmediate — which can raise EditorApplication.hierarchyChanged before it returns. That event is this component's own cue to compare its source set, so a rebuild can be started from the middle of the one already running: the second one tears down objects the first is still holding references to and builds a set the first then finishes over the top of. What comes out draws for a frame and disappears, which is the flicker this guard closes — and it needs a deep hierarchy to show up, because the more children an outline has the more chances the event has to land mid-rebuild.
The nested request is dropped rather than queued, and nothing is lost by that: the outer rebuild ends by recording the set it actually built from, so if the hierarchy really did change underneath it the very next event sees the difference and rebuilds once, cleanly. Queuing it here would instead invite a rebuild that re-arms itself.
Push current values without rebuilding geometry. Prefer this over Rebuild when style topology is unchanged (e.g. Solid↔Sketch). SoftShell / DoubleRing changes still need Rebuild.
Copy this component's current look into a style profile (does not assign the slot).
Events
public event System.Action<IKeylineOutline, KeylinePassKind> PassStyleProfileChangedeventIKeylineOutlinePut a profile in a pass's slot. The four named properties in one call, for code that has a KeylinePassKind in hand rather than a slot in mind.
<inheritdoc/>
<inheritdoc/>
Fields
Resources path of the default outline material.
public const string DefaultResourcesMaterialPathMobileSolid = "4ssets.Keyline/OutlineMobileSolid"fieldstaticResources path of the default Mobile Solid outline material.
Name of the full outline shader.
Name of the Mobile Solid outline shader.
Shader keyword for a rim cut from the texture's alpha (Cutout).
Shader keyword for the sheet variant: flat parts grown in their own plane and given a shell. The name is the one 1.2.0 shipped, kept so variant collections still match.
Compiles the X-Ray see-through-layers test in. Public so the variant stripper can name it — the value itself lives in KeylineShaderIds, beside the property it goes with.
Sprites only: compare this pass at a depth derived from the sorting order rather than from where its quad sits. Never enabled by the 3D controller — a mesh has a real depth buffer to work against, and the keyword costs the early depth test.
Shader keyword for the Neon style.
Shader keyword for the Halo style.
Shader keyword for the Sketch style.
Shader keyword for the Pattern style.
Shader keyword for the Double style.
Shader keyword for the Electric style.
Shader keyword for the Rainbow style.
Shader keyword for the Gradient style.
Shader keyword for the Frost style.
Shader keyword for the Lava style.
Shader keyword for the Smoke style.
Shader keyword for the Crystal style.
Shader keyword for the Water style.
Shader keyword for the Fire style.
Every style keyword, so nothing has to keep a second list of them.
More
Both components clear the whole set before enabling one, and both used to do it by naming each keyword in turn — two lists to extend for every style added, in two files, with the failure being a keyword left enabled from the previous style and a variant that draws something nobody chose. One array, iterated twice.
public const float MaxJoinDistanceMm = 10ffieldstaticLargest Join Distance, in millimetres.
class KeylineOutline2D : IKeylineBlendHost
FourSsets.Keyline · Runtime/KeylineOutline2D.Blend.cs, Runtime/KeylineOutline2D.Children.cs, Runtime/KeylineOutline2D.Diagnostics.cs …
Merge groups for sprites: two outlines in the same KeylineMergeGroup read as one silhouette instead of each drawing its rim across the other's body.
More about KeylineOutline2D
Why the stencil is needed here and not for the rim itself. Keeping an outline's middle from covering its own sprite is already solved without it — _CutoutRimOnly clips the rim to where the sprite's own alpha stops. But that only knows about its own alpha. Suppressing A's rim where it crosses B's body needs A to know B's silhouette, and the stencil buffer is where a silhouette can be left for somebody else to read.
One extra renderer per member does the leaving: the sprite's body stamped with the group's shared value, drawing no colour. Every member's rim then rejects that value, so a rim stops at the edge of any group-mate it overlaps.
Ordering comes from the render queue, not from sorting. Every stamp has to land before any rim, and members can be on any sorting layer in any order. The render queue is a coarser key than the sorting order — the fact that made outlines draw over the whole scene when the shader asked for Transparent+50 — so a stamp queued below the rims is guaranteed first, whatever anyone's layer says. The same lever, pointed the right way.
Inside the transparent phase, deliberately. Stamping from the Geometry queue would be the obvious way to get in first, and it works in URP under both renderers. HDRP loses it: the stencil is its own working space while it draws opaques, and it does not carry a stranger's bits across into the transparent phase. Measured, not assumed. Note that the 3D path never asks it to either — mask and outline both sit in Transparent+50 there.
Costs nothing outside a group. No group, no stamp, no renderer, and the rims keep the neutral stencil state they had before any of this existed.
var outline = GetComponent<KeylineOutline2D>();
outline.OutlineEnabled = true;
outline.SetPassColor(KeylinePassKind.MainOutline, Color.white);Properties
Outline the sprites underneath this one as well.
True when this component was created by a parent outline rather than by hand.
True when a parent outline claimed this one through AdoptChild rather than by growing a generated component.
The outline that currently claims this one — generated owner or adoptive parent — or null when this outline is independent.
The child outlines this one created. Empty unless Include Children is on.
Outlines claimed through AdoptChild. Empty until something asks.
Unity's own answer, surfaced on the interface.
Whether writes fork the slot instead of landing on the shared asset. Turning it off does not un-fork slots already forked — call RevertSharedPassStyleProfiles.
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
public bool XRayEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to XRayOutlineEnabled.
Renamed to XRayOutlineEnabled.
public bool InteriorEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to MainFillEnabled.
Renamed to MainFillEnabled.
public bool XRayInteriorEnabled { get; set; }propertyIKeylineOutlineobsoleteRenamed to XRayFillEnabled.
Renamed to XRayFillEnabled.
Show the outline in the Scene view while not playing.
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
How far the widest rim this sprite draws stands outside those bounds, in metres.
More
The sprite answers for itself rather than asking the profile. A mesh grows a hull and its reach is a length the profile already holds; a sprite's rim is cut out of a padded quad, and the padding is sized in the sprite's own units — through Pixels Per Unit when the width is in texels, which is a number only the sprite has. RimWidthFor is where that arithmetic already lives, so this asks it rather than repeating it.
Screen widths are left out, for the reason given on KeylineStyleProfile.HullWorldReachMeters: a merge decision has to be a fact about the scene, not about where the camera is standing.
The interior slots are not asked. They fill the silhouette and reach nothing past it.
public KeylineStyleProfile Settings { get; set; }propertyobsoleteRenamed to MainOutlineStyleProfile.
Renamed to MainOutlineStyleProfile in 1.3.0.
public KeylineStyleProfile XRaySettings { get; set; }propertyobsoleteRenamed to XRayOutlineStyleProfile.
Renamed to XRayOutlineStyleProfile in 1.3.0.
public KeylineStyleProfile InteriorSettings { get; set; }propertyobsoleteRenamed to MainFillStyleProfile.
Renamed to MainFillStyleProfile in 1.3.0.
public KeylineStyleProfile XRayInteriorSettings { get; set; }propertyobsoleteRenamed to XRayFillStyleProfile.
Renamed to XRayFillStyleProfile in 1.3.0.
Whether writes are currently being held back by an open scope.
Which layer the mirrors are put on. MatchRenderer matches them to the sprite's layer and rendering layers; Fixed keeps them on layer 0 as 1.2.0 did.
In play mode, check every frame whether the sprite changed layer and move the mirrors. Off by default; SyncLayers does the same once, on request.
Stop drawing the outline while the SpriteRenderer is switched off. A sprite hidden by the Reveal recipe keeps its outline.
public int StencilRef { get; }propertyThe stencil value this outline stamps and rejects, or 0 when it is not in a group.
More
Zero is the buffer's cleared state, so it also serves as "no masking": a rim told to reject 0 would reject everything it drew over, which is why the neutral path uses Comp Always rather than a ref of zero.
public bool IsMerged { get; }propertyWhether this outline currently belongs to a group with a stencil value.
Draw the children inside this outline's silhouette rather than beside it.
Share a silhouette with whatever this overlaps, if that one agrees too.
Whether this outline is drawing anything worth merging.
What the field is measured with. Changing it needs the field rebuilt.
The metric the assigned field holds, whatever this component is asking for.
The texture the assigned field was built from, or null if the pairing is unknown.
Texels of empty ground the field holds around the artwork, on every side.
True while the rim is drawn on the texel grid — asked for, and actually available.
More
Both halves matter. Asking for a pixel edge without a pixel field would give a hard threshold on Euclidean distances, which is a crisp rendering of rounded corners: the worst of both, and silent. So the drawing follows the field, and the inspector reports the disagreement.
True when the component wants a pixel edge and the field cannot give one.
The distance field assigned to this sprite, or null.
Range the assigned field covers, in source texels.
Threshold the assigned field was baked at.
Whether the field records the same edge the sprite path draws at.
More
False means the outline is measuring from a contour the artwork is not on, and no amount of tuning width will fix it — the field has to be rebuilt. The reference edge is fixed for sprites (alpha-blended artwork has no material cutoff to agree with), which is why this takes no profile: it used to, and the parameter did nothing.
The scale ratio the assigned field was measured under. One for an ordinary field.
public bool HasRimSdf { get; }propertyWhether this outline has a field to measure against. Without one there is no rim — only the artwork's own shape.
Whether the rim is measured against the distance field at all.
More
Off, the rim falls back to the sprite's own alpha: a silhouette that follows the artwork but cannot be widened evenly, because alpha says whether a texel is covered and not how far the edge is. That is what an outline looks like without a field, and it is worth being able to see on purpose — as a comparison, or as a quality step for sprites far enough away that the difference has stopped being visible.
The assignment itself is left alone, so switching back needs no rebuild and an animation swapping between textures keeps pairing them up while this is off.
Layers this outline's X-Ray may be seen through.
<inheritdoc/>
Empty room around the sprite for the outline to occupy, in the sprite's own units.
More
The rim is drawn on a quad built larger than the sprite by this much, and it is cut off in a straight line wherever it would have gone further. MinimumPadding says how much the passes currently drawn actually need; setting this to at least that is what makes a wide rim fit.
Setting a new value rebuilds the quad at once — the same path an inspector edit takes — so a runtime tool can grow the room and see the outline whole in the same frame.
How many sorting orders above the sprite the outline is drawn. One by default, which is the smallest value that puts it over its own sprite without claiming orders that belong to other sprites.
public bool UsesXRay { get; }propertyWhether either X-Ray pass has a profile, and so needs somewhere to be drawn.
Main Outline profile. Use MainOutlineEnabled to toggle without clearing.
Optional X-Ray profile. Use XRayOutlineEnabled to toggle without clearing.
Optional Main Interior fill profile. Use MainFillEnabled to toggle.
Optional X-Ray Interior fill profile. Use XRayFillEnabled to toggle.
Draw the outline when the scene starts. The sprite half of the mesh controller's property of the same name.
Methods
public void BlendTo(KeylinePassKind pass, KeylineStyleProfile styleProfile, float seconds, bool unscaledTime = false)methodIKeylineOutlineMove a pass to styleProfile over seconds.
More
Colours, widths and the other numbers slide when the two profiles draw with the same mirrors; otherwise the outline fades out, changes over and fades in. The shared assets are never edited, and at the end styleProfile itself is in the slot. Any other write to the pass ends the blend where it is.
Outside play mode, with zero seconds, an empty slot or a null profile, the profile is assigned at once. The two Cutout Overlay passes, which a sprite does not have, are ignored.
| Parameter | |
|---|---|
pass | The pass to change. |
styleProfile | The profile to end on. |
seconds | How long the blend takes. |
unscaledTime | Ignore Time.timeScale, so the blend runs during a pause. |
True while BlendTo is moving this pass.
Jump a running blend to its end: the target profile in the slot, now.
Stop a running blend where it is. The pass keeps the in-between look on its own copy.
Treat child as a child of this outline.
More
The next Rebuild includes it the way a hierarchy child is included: this outline's profiles are pushed onto it. The child's own component is not destroyed. It does not have to sit under this transform.
Calling it again with the same child does nothing. A child already claimed by another outline is released there first. Nested calls push this outline's profiles down the chain: a sprite adopted by a sprite this one has already adopted still follows the figure.
Stop treating child as a child of this outline.
More
The child's component stays where it is and draws for itself again.
Become a child of parent. Same as parent.AdoptChild(this).
Stop being an adopted child. Same as OutlineParent.ReleaseChild(this) when this outline was claimed through AdoptChild.
What each pass resolved to, in one block.
More
Answers which layer X-Ray is on, whether a pass is enabled or only has no profile, and which stencil it rejects, without reading four inspectors. The 3D controller has the same report.
The real GPU-facing values behind all four passes.
More
Deliberately separate from DumpPassDiagnostics, which answers a different question. That one reports what the component decided — which slot has a profile, which pass is on, where X-Ray was placed. This one reports what actually reached the material the renderer is holding, and the two can disagree: a pass can be enabled, with a profile, on the right sorting layer, and still draw nothing because of a depth state or a width that never arrived.
Not gated to the editor. A sprite outline behaving differently in a player build is exactly the case worth logging from a device, and building a string costs nothing until something asks for it.
<inheritdoc/>
<inheritdoc/>
<inheritdoc/>
Change the style of the pass currently being edited, or of the visible rim, forking the profile first when Instance Override says so.
More
The one member of the interface that names no pass, so the one that depends on BeginPassStyleProfileEdit having established which. A style the feature mask has switched off does not render — the shader has no branch compiled for it — so the request falls back to Solid rather than leaving a profile that names one thing and draws another.
World bounds of what this outline covers, or false when there is nothing to measure.
More
The sprite's own renderer bounds, not the padded quad: callers use this to place something relative to the object — a wave's origin, a gauge's anchor — and the padding is empty space that would pull those off the artwork.
public bool IsPassStyleProfile(KeylinePassKind pass, KeylineStyleProfile styleProfile, bool includeForks = false)methodIKeylineOutlineWhether this pass is wearing styleProfile.
More
What counts as wearing it is the one question worth asking here, and includeForks is how you answer it. Writing a look value at runtime with Instance Override on forks the slot: the object stops pointing at the project asset and starts pointing at a private copy of it. From then on "is this the Hostile profile" has two honest answers — no, it is a copy; and yes, it is a copy of Hostile.
False, the default: the slot holds this exact object. A fork is a different object and does not match its original. Use it when the identity is the point — a save system recording exactly what is assigned, or a check that a slot has not been forked behind your back.
True: the slot holds this object, or a fork made from it. Use it when the intent is the point — "is this enemy still showing the Hostile look, whatever I have tinted on top of it". A pulse, a flash, a colour tween all fork the slot, and without this every one of them would make the object stop being recognisably Hostile.
Null asks whether the slot is empty.
Renamed to PassStyleProfile.
Renamed to PassStyleProfile in 1.3.0.
Renamed to PassStyleProfile.
Renamed to PassStyleProfile in 1.3.0. This one was the sprite component's own spelling of the same question, beside PassSettings — two names for one call on one type, which the rename also retires.
public void SetSettings(KeylinePassKind pass, KeylineStyleProfile settings)methodIKeylineOutlineobsoleteRenamed to SetPassStyleProfile.
Renamed to SetPassStyleProfile in 1.3.0.
Renamed to BeginPassStyleProfileEdit.
Renamed to BeginPassStyleProfileEdit in 1.3.0.
public void EndSettingsEdit()methodIKeylineOutlineobsoleteRenamed to EndPassStyleProfileEdit.
Renamed to EndPassStyleProfileEdit in 1.3.0.
Aim the writes that follow at one pass.
More
This used to do nothing, and the reasoning for that was half right. Most of the interface names its pass in the call — SetPassStyleProfile, SetPassEnabled, ForkSlot — and for those a sprite genuinely needs no scope, because it draws all four mirrors at once and has no current pass to activate. But SetStyle takes no pass, and on a mesh it writes to whichever pass this opened. With the pair empty here, it fell through to Primary instead.
So every recipe that changes a style — Impact Wave, Scan Wave, Fill Gauge, Reveal, Damage Flash all open a scope and then call it — was rewriting the sprite's visible rim rather than its own pass. The Main rim is drawn beneath the sprite, so the effect appeared behind the artwork instead of on it, and the pass it was meant for kept whatever style it already had.
Close a burst opened by BeginPassStyleProfileEdit, and do the work it held back.
More
Restores whichever pass the enclosing scope named, or no pass at all when this was the outermost — which is what an unpaired call has always done.
Open a burst of writes to one pass, closed by leaving the using block.
More
The pair above with the ending guaranteed. See KeylineEditScope.
Turn the whole outline on or off. The verb form of OutlineEnabled, spelled the way the mesh controller spells it so one script drives either component.
Set this outline up from one description, in a single build.
More
The four slots assigned one at a time are four rebuilds, and three of them are thrown away by the next assignment. This does the lot inside one edit scope. See KeylineSetup for what the description means — in particular that it describes the whole outline, so a slot it does not name is emptied.
The common case of Configure — profiles and nothing else.
<inheritdoc/>
Has this slot been forked into a copy this object owns?
Fork every slot that holds a profile.
Colour of the pass being edited. Same as setting Color.
Width of the pass being edited. Same as setting Width.
public void Toggle()methodobsoleteFlips Main Outline only. Use MainOutlineEnabled = !MainOutlineEnabled, or
Flips Main Outline only, unlike the mesh component's Toggle.
Copy this object's current look into a profile asset.
More
What an object wearing a forked profile has become is otherwise unreachable: the copy is hidden and not saved, so a look arrived at by gameplay or by nudging values in play mode dies with the session unless it can be written somewhere that persists.
public void SyncLayers()methodPut the mirrors on the layer MirrorLayer asks for, now. Call it after changing the sprite's layer from code when FollowLayerChanges is off.
Called by KeylineMergeGroup when the group's allocation changes — it is taken on enable and given back on disable, and a member cannot see that happen.
Re-apply the merge rules after the fields behind them were written directly.
More
The Inspector does not go through the property setters. PropertyField edits the serialized field, so everything the setters do — telling the descendants their answer changed, asking clustering to re-run — was skipped, and both switches looked stuck in their on state. This is the same work, asked for once after the write.
Hand this outline a field and say which sprite texture it was built from.
More
For sprites created while the game is running. A component that was placed in the editor finds its own field: the name says which texture it belongs to and the asset database answers where it is. A player build has no asset database, so an outline added to an object spawned at runtime has nothing to look in — the rim silently falls back to the artwork's own alpha, which follows the shape but cannot be widened evenly. The failure only appears in the build, because in the editor the lookup works.
Hold the field somewhere a build will ship it — a list on a ScriptableObject, a field on the spawner — and hand it over here after assigning the sprite.
The pairing is remembered, so a sprite swapped away and back finds the same field again. That is what makes this usable for animation: register one per frame texture and the outline follows the frames.
| Parameter | |
|---|---|
source | The sprite texture the field describes. |
field | The baked field. |
The width a pass may actually use, capped at what the field can describe.
More
Past its range the field saturates — every texel beyond reads as the same "at least this far", which is not a distance and cannot place an edge. Left uncapped the rim went on growing into that plateau and came apart: flat facets where the shape was furthest out, and on zoom-out a filled rectangle the size of the whole quad.
Stopping at the range is the honest failure. The outline reaches as far as it was built to reach and no further, and the inspector says to rebuild wider — which is a setting to change, not a defect to puzzle over.
Screen-space width is left alone: it is not in world units, so there is no fixed number of texels to compare against. The camera decides, and the shader's own clamp catches it there.
Where the X-Ray passes will be drawn, or false when the scene declares no occluders — in which case they have nothing to draw anyway. For tooling; the value is derived, not set.
Profile for a pass. Null leaves that pass off, and null is also the answer for the two Cutout Overlay passes, which a sprite does not have — see KeylinePassKind.
public void SetPassStyleProfile(KeylinePassKind pass, KeylineStyleProfile styleProfile)methodIKeylineOutlineAssign a profile to a pass and rebuild that pass.
Rebuild geometry and materials from the sprite as it is now.
Re-read the profiles without rebuilding geometry.
Is this pass drawn at all?
More
Two conditions, and they mean different things. A slot with no profile is a pass that does not exist; a toggle turned off is a pass that exists and is being held back. Keeping them separate is why the toggle does not clear the slot — turning X-Ray off for a scene should not lose the profile somebody chose for it.
Turn a pass on or off without clearing the profile in its slot.
<inheritdoc/>
Events
<inheritdoc/>
public event System.Action<IKeylineOutline, KeylinePassKind> PassStyleProfileChangedeventIKeylineOutline<inheritdoc/>
<inheritdoc/>
class KeylineMergeGroup : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineMergeGroup.cs
Force-merge outlines into one hollow silhouette even when their subjects do not intersect.
More about KeylineMergeGroup
The strongest of the three things that can join a silhouette — see KeylineMergeResolver. Everything in one group is one silhouette, and an outsider that overlaps a member joins the group rather than pulling a member out of it.
Two ways to say who is in. By default the group takes everything under it, which is what it has always done, with an ignore list for the child that should keep its own outline. Turn that off and the members are named one by one instead, and they can live anywhere in the scene — useful when the objects that belong together in a silhouette do not belong together in the hierarchy.
Works on meshes and sprites alike: a KeylineOutline2D joins the same group and shares the same stencil value. Automatic merging is meshes only — the clustering reads controller bounds — so sprites are merged by putting them in a group.
Properties
Shared stencil for primary (visible) outlines in this group.
Shared stencil for X-Ray / occluded companion outlines in this group.
Whether this group holds a pair the pool currently stands behind.
More
The generation check is the load-bearing part. With "Reload Domain" and "Reload Scene" both disabled the group survives into Play holding a number the pool has forgotten — and the pool will hand that same number to somebody else, silently merging two unrelated silhouettes. A live, enabled group re-ensures its pair on the spot (OnEnable will not run again to do it); a disabled one simply answers no, because re-allocating from a getter after OnDisable released the pair is the exact leak this class once had.
Whether membership comes from the hierarchy. False means the explicit list decides, and descendants are not members unless they are named.
Whether membership comes from the hierarchy. False means the explicit list decides, and descendants are not members unless they are named.
Methods
Is this object excused from the group and from automatic merging?
The outlines this group currently covers. Built fresh on request — for the inspector's readout and for anything that wants to know without guessing at the rules.
Push a stencil refresh to everything this group covers, and to everything it used to.
More
The second half is easy to forget and impossible to miss once it bites: an object dropped from the list has to be told, or it keeps rejecting a stencil value nobody stamps any more and its outline quietly stops drawing.
class KeylineOccluder2D : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineOccluder2D.cs
Marks one sprite as something that hides what is behind it, so 2D X-Ray has an occluder to work against.
More about KeylineOccluder2D
Why occluders have to be declared at all. In 3D the depth buffer is the register of occluders: everything opaque writes to it, and Keyline just reads the result. A 2D scene leaves that buffer empty, and sorting order alone does not answer the question either — it says what is drawn on top, not what counts as an occluder. A foreground bush and a stone barrier can sit on the same layer with the same order; only the person building the scene knows which one the player should be able to see through.
When to use this rather than the layer registry. This component names one specific sprite. KeylineOccluderLayers2D names whole sorting layers at once and is usually the better fit for scenes organised the normal way — a Occluders layer, a Foreground layer. This one is for the exceptions, and it covers both directions: a sprite that should occlude although its layer is not declared, an occluder whose cutoff has to differ from everyone else's, and — with IgnoreXRay — a sprite on an occluder layer that should not occlude at all.
A sprite carrying this component is skipped by the registry entirely, so the two never fight over it. That is what makes the exception possible: presence is the signal, and what the component then does is up to its own settings.
What it draws: nothing. A child renderer writes depth in the shape of the sprite's own alpha and masks its colour off entirely. Nothing about the sprite's appearance changes.
Costs one extra draw call, in the Geometry queue, with no colour work.
Properties
Whether this sprite is excused from occluding. True makes it an exception to whatever its sorting layer is declared as.
How opaque a pixel must be to occlude. 0 to 1.
Methods
public void Refresh()methodRebuild the proxy from the sprite as it is now.
class KeylineOccluderLayers2D : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineOccluderLayers2D.cs
Declares whole sorting layers to be occluders, so 2D X-Ray works without a component on every sprite that should hide something.
More about KeylineOccluderLayers2D
What it does not do. It does not make sorting order occlude by itself. Sorting order is one number for a whole sprite and knows nothing about where the occluder has a doorway; occlusion that follows the artwork needs the occluder's shape in the depth buffer, and that still means an invisible depth proxy per occluder. What this removes is the manual step — you name the layers, and the proxies are built and kept in step for you.
Why it lives in the scene rather than on each outline. The depth buffer is shared. A proxy built for one sprite occludes every Keyline 2D outline behind it, not only the one that asked for it, so a per-object "which occluders apply to me" setting would be describing something it cannot actually control. One authority per scene tells the truth.
Choose the layers deliberately. "Everything drawn on top is an occluder" is the wrong rule and the reason this is a list rather than a checkbox: particles, damage numbers, foreground dressing and screen-space decoration usually sort above the action and would start hiding things nobody meant to hide.
Sprites carrying KeylineOccluder2D are left alone — that component is the way to say something specific about one occluder, and the registry must not fight it. Sprites carrying KeylineOutline2D are skipped too: an outlined object is the subject of the effect, and letting it write depth would have it occlude its own X-Ray.
Properties
public static KeylineOccluderLayers2D Active { get; }propertystaticThe registry the scene is using, or null if there is none.
More
One per scene. A second is not an alternative opinion: the depth buffer is shared, so whatever either of them declares hides things for everyone. Extras are reported rather than merged.
How many occluders this registry is currently holding proxies for.
The sorting layers declared as occluders, as written in the inspector.
Whether the scene is swept for occluders on its own.
Methods
Create a sorting layer of this name in the project if it does not already exist.
More
Sorting layers live in Project Settings, not in a prefab. A component that names "Occluder" is silent in a project that has never defined that layer — every sprite authored on it falls back to Default, and 2D X-Ray has nothing to test against. Call this before instantiating such a prefab so the layer is there when Unity binds the sprites' sorting IDs.
Only ever called by whoever wants the layer, and never by this component. Writing Project Settings is not something a package may do on its own schedule, and this used to be reached from the sweep — which runs in edit mode, on a timer. The one caller in the package is the demo, which owns the prefab that is authored on its own layer; the component itself asks whether a layer exists and says so when it does not.
Editor only as a mutation: a player cannot edit TagManager, so this then returns whether the layer is already in the built project. False means it is missing and sprites that referred to it have landed on Default.
Declare the occluder layers from script, replacing whatever the inspector says.
More
The list is the gate every occluder passes through: Add refuses a sprite that is not on one of these layers, and with the sweep switched off there is otherwise nothing that would ever build the list. A scene assembled at runtime — one that instantiates its level, or ships a prefab into a project whose sorting layers it does not control — has no inspector to fill in beforehand.
Already-claimed occluders are re-checked immediately, so a sprite on a layer that was just dropped stops occluding rather than lingering until the next sweep.
public void Rescan()methodSweep the scene now. Worth calling after spawning occluders if the interval is long, or if it is zero because the scene is static.
public static void RescanAll()methodstaticSweep every registry in the scene.
Claim one sprite as an occluder, without sweeping the scene.
More
The cheap way in, and the one a project that spawns its own occluders should use: a sweep looks at every sprite in the scene to find the one you already have a reference to. Safe to call twice — a sprite already claimed is left alone.
The sprite still has to qualify: enabled, with artwork, on one of the sorting layers listed above. False says it did not, rather than claiming it quietly and leaving a proxy that draws nothing.
Release one sprite, taking its proxy down with it.
More
Worth having for the same reason as Add: without it the only way to let an occluder go is a sweep that has to notice it stopped qualifying, which means changing its sorting layer or disabling it purely to be noticed. Destroying the sprite needs no call at all — the next sweep or sync drops the entry and frees its proxy.
class KeylineDrawConfig : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineDrawConfig.cs
Scene host for an optional KeylineCustomDrawConfig. When this component is enabled, every 3D Keyline outline reads that asset. When it is absent, outlines use the hardcoded factory (no ScriptableObject).
Properties
public static KeylineDrawConfig Active { get; }propertystaticThe Draw Config in force: the first one enabled that is still enabled, or null.
Custom draw asset assigned to this host. Setting it on the active host raises PresenceChanged.
Assigned custom asset, or null to use the memory factory.
More
Null as well while the project has Custom Draw Config switched off: the component and its asset stay where they are, and every outline draws with the factory table.
Events
public static event Action PresenceChangedeventstaticRaised when the Draw Config in force changes: a host is enabled or disabled, its asset is reassigned, or it is edited in the Inspector. Outline controllers listen to reapply their draw settings.
Style profiles
The assets that hold the look.
class KeylineStyleProfile : ScriptableObject
FourSsets.Keyline · Runtime/KeylineStyleProfile.cs
Live look settings for KeylineOutlineController appearance and style parameters. Owns its own validation — controllers never write clamps back into this asset.
More about KeylineStyleProfile
This is a shared asset. A profile assigned to a hundred objects is one object in memory, so writing to its fields at runtime changes all hundred at once, and in the editor the change survives play mode because a ScriptableObject edited at runtime is the same object that sits on disk. Gameplay code should therefore go through KeylineOutlineController: its look properties fork the profile into a copy the object owns before writing. Where a whole batch of fields needs changing, ask the controller for that copy with ForkSlot(pass) and write to it directly.
// A runtime copy, so the project asset is never edited from play mode.
KeylineStyleProfile hover = baseProfile.CloneForRuntime("Hover");
hover.color = Color.yellow;
hover.RaiseChanged(); // tell every outline using it
outline.SetPassStyleProfile(KeylinePassKind.MainOutline, hover);Properties
public KeylineExpandMode expandMode { get; set; }propertyobsoleteObsolete.
Always KeylineExpandMode.Mixed. Setting it does nothing.
public KeylineMixedDetectMode mixedDetectMode { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetThicknessCm { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetFlatness { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public int multiRaySamples { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float rayConeDegrees { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public int minSheetFaces { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public int minOutlineTriangles { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float separatePartCm { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public KeylineExpandCenterMode expandCenterMode { get; set; }propertyobsoleteObsolete.
Always KeylineExpandCenterMode.MeshBounds. Setting it does nothing.
public Vector3 expandCenterCustom { get; set; }propertyobsoleteObsolete.
Always zero. Setting it does nothing.
public float miterLimit { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float planarWidthScale { get; set; }propertyobsoleteObsolete.
Always 1. Setting it does nothing.
public float planarHeightScale { get; set; }propertyobsoleteObsolete.
Always 1. Setting it does nothing.
public bool sheetSolidifyEnabled { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetVolume { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public bool sheetVolumeSeparate { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetThickness { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetLateral { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetThickenX { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
public float sheetThickenY { get; set; }propertyobsoleteObsolete.
Hull geometry before 1.3.0. Read once into an older component.
Which shader program this profile is authored for — decided by the asset's type, not by a field on it.
More
shaderProfile still holds the answer, because everything that reads it — keywords, the variant stripper, the 2D component — is written against that field and has no business knowing about types. Validation writes this into it, so the field is a cached fact rather than a choice, and it cannot disagree with the program that will actually draw.
True when the Mobile Solid (cheap) shader profile is selected.
Style actually used for topology / rendering (disallowed → Solid).
More
A style the project has switched off in Project Settings ▸ Keyline ▸ Features resolves to Solid here and nowhere else — the style field keeps the authored value, so switching the style back on restores the look untouched.
public KeylineExpandMode ResolvedExpandMode { get; }propertyobsoleteObsolete.
KeylineExpandMode.Mixed where this profile builds sheets, otherwise KeylineExpandMode.Normal.
public bool DrawsCutout { get; }propertyWhether a rim wearing this profile is cut from the source material's alpha: authored on, and the project has not switched Alpha Cutout (3D) off.
Whether this profile is drawn by the cheap Mobile Solid program. False for an ordinary profile, and for a Mobile Solid one while the project has Mobile Solid Profiles off — it is then drawn as Solid by the Full program, which is in every build.
Whether the rim pulses: authored on, and Pulse not switched off in the project.
The blend the rim draws with. Default while the project has Blend Modes off.
Edge Softness as it will be drawn: zero for a rim cut from alpha — which has no hull to blur — and zero while the project has Soft Edge off.
Halo Occlusion as it will be drawn. Ignore Depth while the project has it off.
Whether a rim wearing this profile is blurred and that blur is cut against scene depth — the combination that finishes most of the intersection clip's work and leaves the rest to it.
More
Cutout is excluded for the same reason it is excluded from Edge Softness itself: its edge comes from a distance field and the screen-space pass never runs on it.
On the profile rather than in either place that asks, because both have to agree: the hull decides whether to build the clip mirrors from it, and the inspector decides whether the switch is still the reader's to set.
The rim's full width, styles included, in whatever unit widthMode is counting in.
More
Not the Width slider. Double reaches past its inner band by the gap and the outer ring; Frost by its shell. Halo does not — its layers are fractions of Width, so the outermost lands exactly on it.
Kept on the profile rather than in a component: the sprite side carried its own copy of this switch, and the mesh side sizes its shell mirror from the same numbers. Two switches over the same styles agree until the day one of them gains a case.
The most Pulse ever widens the rim, as a multiplier.
More
The top of the swing, not the current phase: anything sizing a figure from this wants one answer for the whole cycle, or the figure breathes with it.
How far past an extruded hull's own surface this look reaches, in metres — 0 when the answer is not a length in metres at all.
More
What it is for. Merge When Overlapping compares world bounds, and Unity measures the source mesh: the hull is grown in the vertex stage, so the outline itself lies outside every bounds the engine reports. Two objects a rim-width apart therefore looked separate to the resolver while their outlines were plainly touching on screen. This is the number that closes that gap.
Not the same as the Soft Edge reach. That one is deliberately generous — dilating a depth field too far costs iterations and nothing else. Merging is a visible decision: expand too far and two objects meant to read as separate are fused into one silhouette, and the line between them disappears. So this follows the hull's own arithmetic rather than a headroom multiplier.
Screen widths answer 0, on purpose. A width in screen pixels is a different length in metres at every distance, so honouring it would make merging a property of where the camera is standing — silhouettes fusing and splitting as somebody walks towards them, with stencil ownership churning behind it. A merge decision has to be a fact about the scene. Bodies still merge on overlap; only the rim's own reach is left out. Texel widths answer 0 here too, for the plainer reason that the conversion into metres needs a sprite, which is the sprite component's to supply.
Cutout answers 0 as well: on a mesh its edge is dilated from the texture's alpha inside the surface, and nothing is extruded past it. A sprite's rim is drawn from a field too but on a padded quad that really does stand outside the artwork, which is again why that component answers for itself.
Signature of everything that changes mirror topology or the baked outline mesh. Controllers compare it to decide Rebuild (expensive) vs RefreshAppearance (cheap).
Methods
Notify listeners that look values changed (call after editing fields from code).
Does style read this group at all? Mirrors the style branches in the shader, plus the mirror topology chosen on the CPU — Shell exists for Neon and Frost because those build a second hull, not because of anything in the fragment.
True when a Full-shader feature bit is enabled on this profile and the project has not switched the feature off (always false on Mobile Solid).
More
Everything that decides how the profile is drawn asks this. Validation asks IsFeatureAuthored instead, because it writes back what it concludes — and a project setting must never rewrite a profile, or switching a feature back on would find the values it had hidden gone.
True when a Full-shader feature bit is enabled on this profile, whatever the project says. The authored answer, for validation.
Mobile Solid renders Solid only. Under Full every style is available. Styles used to be gated by the Feature Mask as well, which meant a profile could sit on a style whose bit was off and silently render as Solid — the trap that produced the Gradient bug. The mask no longer decides what may render; it only records which shader variant the build needs, and its style bits are derived from style.
Feature-mask bit a style cannot render without (None for Solid).
Is the cutout path a choice for this subject, or the only thing it does?
More
On a mesh it is a choice: the rim normally comes from an extruded hull, and cutout swaps that for widening the material's alpha. On a sprite there is no hull to extrude — widening the alpha <em>is</em> the outline — so cutout is not a mode, it is the mechanism, and the component turns it on whatever the profile says.
Which is why useCutout being off must not hide Cutoff and Rim Softness from a profile used by sprites. Those two are the only controls that shape a sprite's rim, and they were unreachable.
True when the style reads a spatial coordinate at all. Solid, Neon, Halo and Double are rim/width driven, so Style Space would be a no-op for them.
Not every style supports every space. Pattern and Gradient drop World: both measure along a single axis from the pivot, which for an object with no rotation is the same number in world space as in object space — a mode that behaved like a duplicate of its neighbour most of the time.
public static KeylineStyleSpace LegacySpaceFor(KeylineStyle style, KeylineRainbowMotion motion)methodstaticSpace a style used before Style Space existed. Needed because the legacy behaviour differs per style, so no single default value can preserve every profile.
Alpha of one Halo layer.
More
Layers are concentric hulls: each covers everything inside it, and with additive blending the brightness at a radius is the sum of every layer wider than it. So a layer must contribute the difference between neighbouring levels of the target curve, not the level itself — otherwise the layers pile up into an arbitrary ramp instead of reproducing the falloff.
The falloff is a straight ramp from the object out to nothing, and has no controls. It had two — an alpha for the outer edge and a shape for the fade — and they were removed rather than kept: measured on a real profile, moving the first from 0.65 to 0.82 changed the halo's interior from 0.840 to 0.849, because the layers carry differences and therefore always summed back to the curve's value at the inner edge, which was pinned. Two sliders that between them reached one thin band are worse than no sliders at all — Width and Layers say everything this style needs to be told.
Width of one Halo layer as a fraction of the full outline width.
Obsolete.
KeylineExpandMode.Mixed where this profile builds sheets, otherwise KeylineExpandMode.Normal. The argument is ignored.
public static void CopyValues(KeylineStyleProfile source, KeylineStyleProfile destination)methodstaticOverwrite every serialised value of destination with the values of source, leaving the object identity alone. Used to reset a per-instance clone back to its shared profile without allocating a new clone: a highlight that swapped a colour in should be undoable in one call, and anything that hands out references to the settings object keeps working because the object is the same one. Goes through JsonUtility rather than a field-by-field copy on purpose — a hand-written copy silently misses fields added later, and this class gains fields regularly.
A private runtime copy of this profile, for edits that must not touch the asset.
More
Not saved with the scene. Destroy it when done (Destroy(copy)). To give one outline its own copy of a slot, prefer IKeylineOutline.ForkSlot, which cleans up after itself.
KeylineStyleProfile alert = baseProfile.CloneForRuntime();
alert.color = Color.red;
outline.SetPassStyleProfile(KeylinePassKind.MainOutline, alert);
Clamp every field into its legal range and drop selections the current Shader Profile / Feature Mask cannot render. Safe to call at any time.
Events
Raised after this asset validates itself. Controllers listen so that editing the asset in the Project window live-updates every scene object that references it.
Fields
The look this profile draws.
public Color colorfieldMain colour of the outline.
What this profile is drawn on, which decides the controls the inspector shows.
Second colour of the styles that use one.
public float widthfieldHow far the outline reaches out from the surface.
World-space halo on the outer rim.
Whether the soft edge stops where its rim stops.
World keeps the outline in proportion to the object: it thins out with distance and goes away with the thing it is on.
How the outline is composited over what is behind it.
Which program draws a pass using this profile. Written by validation from AuthoredShaderProfile, never chosen here — an ordinary profile is Full and a KeylineMobileSolidStyleProfile is Mobile Solid.
More
Serialized rather than derived on the spot because everything that consumes it — the keyword pass, the variant stripper, the 2D component — is written against a field and has no business knowing about asset types. Kept public for the same reason.
Full shader only: disable unused features to strip HLSL (Solid always on).
Coordinate space the style's pattern / noise / gradient lives in.
Let the transform's non-uniform scale stretch the style, stored as the opt-out.
More
Inverted so the serialized zero is the new default. Unity gives a bool that is missing from an asset false, never the C# initialiser — so a field written as = true would read as "on" for new profiles and "off" for every profile authored before it existed. Storing the opt-out makes the zero mean "corrected", which is what every existing profile should get, and the inspector shows it the way round the reader thinks about it.
Only Object style space is affected. World, Screen and UV do not measure in the model's local units, so there is no aspect of the transform in them to remove.
public float opacityfieldOverall transparency of the whole outline, on top of the colours' own alpha.
Brightness multiplier for the styles that emit.
Neon / Frost / Water: how quickly the view-facing rim fades.
Frost: reach of the outer halo, in the same units as Width.
Frost: opacity of the outer halo relative to the core band.
Sketch / Electric: how far the line is pushed around by the noise.
Sketch / Electric: how fast the noise re-rolls.
Pattern: greyscale mask tiled along the outline.
Pattern: tiling of the mask.
Pattern: scroll speed of the mask along X and Y, in tiles per second.
Pattern: contrast applied to the mask before the threshold — sharpens soft artwork into a crisp stencil.
Pattern: mask level that separates visible from clipped.
Double: thickness of the inner ring, in the same units as Width.
Double: clear space between the two rings.
Double: thickness of the outer ring, drawn in the Secondary colour.
Rainbow: how tightly the spectrum repeats along the ramp.
Rainbow: how fast the bands travel.
Rainbow: colour purity.
Rainbow: Vertical sweeps the bands along the vertical axis of the active Style Space — the object's own on a new profile, the screen's under Screen Space.
Circular Rainbow: object-local X of orbit center (0 = pivot).
Circular Rainbow: object-local Y of orbit center (0 = pivot).
Rainbow, Vertical: sweep direction in degrees. 0 is the vertical axis this style always used, so a profile that leaves it there does not change.
Rainbow, Vertical: lean of that direction out of the style space's XY plane. Meshes in Object or World space. Screen, UV and sprites stay in the plane.
Gradient: how wide the blend between the two colours is.
Gradient: slide the ramp along its own direction.
Gradient: direction of the colour sweep in degrees.
Gradient: how far the direction set by gradientAngle leans out of the style space's XY plane, in degrees. Meshes, Linear in Object or World space and Radial.
Gradient: Linear ramps along the angle above; Radial ramps outwards from a point in world space.
Gradient, Radial and Planar: the world-space point the ramp is measured from.
Gradient, Planar only: which way the plane faces.
Gradient, Radial only: whether Radius is multiples of the object's size or metres.
Gradient, Radial only: where the boundary between the two colours sits.
Gradient: 0 leaves the ramp as it is.
Gradient, Radial only: how much further the ramp reaches along the Angle direction.
Halo: how many steps the falloff is built from.
Frost: density and brightness of the shimmering ice sparks.
How large a patch of plain ice is left where the surface turns to face the camera.
public bool pulsefieldBreathe the outline's width and intensity over time.
Pulse: beats per second.
Pulse: how deep the swing goes, as a fraction of the resting width.
Pulse, pixel art only: how far the outline dims as it reaches full stretch.
Pulse: how much brighter the outline gets at the top of the swing.
Follow the source material's alpha cutout instead of the polygon edge — grass cards, foliage, decals.
How soft the outline's outer edge is, as a fraction of its own width.
How far the outline continues underneath the cutout artwork, in screen pixels.
How far the crust has broken open, from a solid dark shell to nothing but glow.
Size of the crust cells.
How fast the molten core moves under the crust.
Brightness of the hot core inside the cracks, over the outline's colour.
Direction the flow runs, in degrees.
How much the flow curls instead of sliding straight.
How fast the glow breathes, in beats per second.
How deeply the pulse swings, as a fraction of the heat.
How much of the cloud survives.
Size of the billows.
How fast the cloud drifts.
Which way it drifts within the style space's XY plane, in degrees.
How far the drift leans out of that plane, in degrees.
How hard the cloud rolls as it travels.
How far along the drift the cloud reaches, as a fraction of the shape.
How raggedly the cloud ends, once Height has decided where.
Size of the facets.
Where the light falls from, in degrees.
How fast the light turns around the stone, in turns per second.
How much fire the stone has.
How far towards white a lit facet is allowed to go.
Brightness of the cut edges between facets.
How even the facets are, and with it the smallest one you can get.
How much each facet darkens towards its own middle.
How far the facets split the colour between them.
How fast the surface flows.
Which way it flows, in degrees.
Size of the ripples.
Strength of the caustic streaks — the bright threads where the surface focuses light.
How much the flow curls rather than sliding straight.
Sheen along the ripple crests, where the surface turns towards the viewer.
How fast the tongues rise.
Which way they rise within the style space's XY plane, in degrees.
How far the rise leans out of that plane, in degrees.
How far up the tongues reach, as a fraction of the shape.
How many times longer the tongues are than they are wide.
Size of the tongues.
How much the flames writhe as they climb.
Brightness at the base of the flame, over the outline's colour.
How much the flame gutters — the whole of it breathing together, and its tongues dropping out of step with each other.
Which faces draw this pass (not the interior stencil mask).
Paint each pixel once, however many times the outline covers it.
public const float MinimumWidth = 0.0001ffieldstaticSmallest rim width the API and the inspector accept, in the profile's units.
class KeylineMobileSolidStyleProfile : KeylineStyleProfile
FourSsets.Keyline · Runtime/KeylineMobileSolidStyleProfile.cs
A style profile drawn by the Mobile Solid shader: Solid, cheaply, with no style branches.
More about KeylineMobileSolidStyleProfile
A type rather than a switch, and that is the whole point. This used to be a value in a dropdown on the ordinary profile, which read as a performance setting and was not one: it changes which program draws, and with it the set of values the asset may meaningfully hold. Halo on such a profile stayed Halo everywhere the inspector could say so — the field, the header, the parameters — and drew a plain line. Nothing was wrong with the asset except that it described a shader it was never going to be drawn by.
The same mistake has a scar in this package already: styles were once gated by the variant mask, so a profile could sit on a style whose bit was off and silently render as Solid. That was fixed by taking the decision away from the mask. This takes it away from a dropdown, for the same reason — an asset should not be able to promise what its program cannot draw.
It adds no fields. Everything this profile holds, an ordinary one holds too; what the subclass contributes is one answer, and the base class asks for it whenever the asset validates. That is also what makes an existing asset convertible in place — the serialized data is identical, so only the script reference has to change, and every reference to the asset survives.
Properties
<inheritdoc />
class KeylineStyleDefaults
FourSsets.Keyline · Runtime/KeylineStyleDefaults.cs
Reference look for every outline style — the single source of truth behind the shipped Profile assets, the New button and Reset to Style Defaults. These values used to live in DemoDirector, which buyers are meant to be able to delete, and were duplicated a second time inside the Profile assets themselves. Keeping them here means the two can no longer drift apart.
// A new profile at the reference look of a style.
KeylineStyleProfile fire = KeylineStyleDefaults.Create(KeylineStyle.Fire);
outline.SetPassStyleProfile(KeylinePassKind.MainOutline, fire);Methods
Overwrite the look of target with the reference values for style. The Feature Mask is deliberately left alone: which variants a build compiles is a build-size decision, not a look. The one exception is the bit the restored style itself needs — without it validation would silently bounce the style back to Solid. Pattern Texture is also kept, since it is an asset the user picked. Shader Profile goes back to Full — see below for why it does not keep that company.
A Settings instance preloaded with the reference look for a style.
Feature-mask bit a style cannot render without (None for Solid).
class KeylineStyleFacts
FourSsets.Keyline · Runtime/KeylineStyleFacts.cs
Things that are true of a style regardless of any profile, asked by code that cannot see how the controller builds its mirrors.
More about KeylineStyleFacts
The inspector and the demo both need to know whether a style can take the overlap fence, and neither can reach GetTopology — it is private to the controller, and the enum it returns is private too. Rather than widen either, the one fact they need lives here.
Methods
Whether Single Layer means anything for this style.
More
False for the three built out of stacked hulls. Halo is N concentric shells whose overlap <em>is</em> its falloff; Frost is a halo around a core; Double is three rings. A fence would keep whichever of them drew first and discard the rest, which is not a tidier version of the style but the absence of it.
This is the same set as the non-NoShell cases of the controller's GetTopology, and the controller checks both. Two lists that could drift is worse than one, but requiring both to agree means a drift turns the fence off rather than letting it through onto a style that cannot survive it.
class KeylineParameters
FourSsets.Keyline · Runtime/KeylineParameters.cs
Which styles read a parameter, which subjects read it, and the range it is usable in.
More about KeylineParameters
Why one table. Three places used to claim to know these things independently: the field's own attributes held a bound, StyleUses held the style list, and the demo's panels held a considered upper limit the asset did not have. Nothing kept them in step, and an audit found fourteen parameters where the demo knew a usable range and the profile only knew a minimum — plus one control that was live on Neon and hidden, and four that were shown on sprites and read by nothing there.
That is the same failure this package's shader header warns about: two copies that agree until the day one is edited alone. Here it is four. Adding a parameter is now one row, and the inspector, the clamp and the demo cannot disagree because there is nothing left to disagree with.
Range means usable, not legal. Glow accepts any positive number and stops doing anything past about six; the slider says six. Where a value genuinely has no ceiling the row says so and the control stays a plain field.
Properties
public static IReadOnlyList<Row> All { get; }propertystaticEvery row, in the order the inspector draws them.
Methods
The row for a field, if the table describes it.
Whether a control should be on screen at all for this style and subject.
More
A field the table does not describe is always shown. Silence means "not yet classified", and hiding a control that turns out to matter is the expensive mistake; showing one that does not is merely untidy.
Hold a value to its row's range.
More
Called from OnValidate, so a value written from script cannot land somewhere the inspector would not let you drag it — which is how a profile ends up with a number nobody can reproduce.
struct KeylineParameters.Row
FourSsets.Keyline · Runtime/KeylineParameters.cs
One parameter, and everything the editor needs to know about drawing it.
Properties
public bool Bounded { get; }propertyTrue when this row wants a slider rather than a free field.
Methods
True when style reads this parameter, or when the row lists no styles.
True when the parameter is drawn on any of the given subjects.
Clamps value to the row's range; only the lower bound applies when the row is unbounded.
Fields
Serialized field name on KeylineStyleProfile.
Inspector label. Null uses Unity's own prettified field name.
Subjects the parameter is drawn on.
Styles that read it. Empty means every style does.
Lower bound of the usable range.
Upper bound, or float.PositiveInfinity when there is none.
Short explanation of what the parameter does, or null.
Constructors
Creates a row. Pass float.PositiveInfinity as max when there is no upper bound.
enum KeylineParameters.Subjects
FourSsets.Keyline · Runtime/KeylineParameters.cs
What a parameter is drawn on. Both is the common case.
enum KeylineStyleProfile.KeylineSubjectKind
FourSsets.Keyline · Runtime/KeylineStyleProfile.cs
What a profile is being applied to.
enum KeylineStyleProfile.OutlineStyleParameter
FourSsets.Keyline · Runtime/KeylineStyleProfile.cs
Groups of look parameters, so a UI can ask what a style actually reads. The answer used to be duplicated in the inspector and in the demo HUD, each with its own idea of the rules — at one point they disagreed about whether Electric reads Glow (it does). One table here, consulted by both, checked against the shader.
Values
GlowvalueGlow intensity.
SoftFalloffvalueSoft falloff of the view-facing rim.
ShellvalueWidth and opacity of the outer shell hull.
SecondaryColorvalueSecondary colour.
SketchvalueSketch amount and speed, shared by Sketch and Electric.
PatternvaluePattern texture, scale, scroll, contrast and threshold.
DoubleRingvalueDouble's inner width, gap and outer width.
RainbowvalueRainbow scale, speed, saturation, motion, centre, and the vertical sweep.
GradientvalueGradient shape and its per-shape controls.
HalovalueHalo layer count.
FrostSparklevalueFrost sparkle and band fade.
BaseColorvalueBase colour.
WidthvalueShared outline width.
LavavalueControls only Lava reads, such as crack, flow and heat.
SmokevalueControls only Smoke reads, such as density, drift and dissolve.
CrystalvalueControls only Crystal reads, such as scale, glint and dispersion.
WatervalueControls only Water reads, such as flow, caustics and sheen.
FirevalueControls only Fire reads, such as rise, height and flicker.
Recipes
Ready-made behaviours under Add Component ▸ Keyline ▸ Recipes.
interface IKeylineHighlightTarget
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineHighlightTarget.cs
Something a highlight state can be held on. Exists so the pointer and the selection do not have to know whether they are pointing at one object or at a squad. Both answer the same three questions, and everything that drives a highlight can be written once against this rather than twice against two types.
Methods
void Push(string id)methodHold a state.
void Pop(string id)methodRelease a state.
bool IsHeld(string id)methodTrue while the state is held.
interface IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylinePassWriter.cs
Implemented by every recipe that writes a pass slot directly rather than going through KeylineHighlight. The arbiter solves this problem for the rim pass: named states with priorities, highest wins, releasing one falls back to the next. Nothing equivalent exists for the recipes that paint a slot themselves — Fill Gauge, Damage Flash, Impact Wave, Reveal, X-Ray When Hidden — because each of them drives the whole profile, not a look on top of one, and there is no meaningful way to blend a gauge level with a shockwave. So two of them on the same pass is not a case to arbitrate, it is a setup mistake. What was missing was any way for the user to find out: the failure is that whichever effect ends first restores the slot from the shared profile and wipes what the other one was drawing, which looks like a bug in the surviving effect rather than a conflict between two.
Methods
True when this component paints pass itself.
class KeylineBlink : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineBlink.cs
Blinks a highlight state to draw the eye — the objective, the tutorial step, the thing the player is being told to click. Holds and releases a state on a timer rather than switching the outline on and off itself, so blinking composes with everything else: blink an object that is also selected and the selection look stays put underneath, because the arbiter keeps showing the higher state. Unscaled time by default. An attention cue that stops in a paused menu is a cue that fails exactly when the player is most likely to be lost.
Properties
public bool IsPlaying { get; }propertyHighlight state the blink holds. Give it one of its own rather than borrowing Hover — otherwise the pointer passing over the object releases the state and cancels the cue.
Methods
public void Play()methodStart blinking with the authored count.
Start blinking a given number of times. 0 blinks until stopped.
public void Stop()methodStop and release the state.
Fields
Fires every time a run begins, including a run started by Play On Enable.
Fires when the run ends: the count ran out, Stop was called, or the object was disabled.
class KeylineClickSelection : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineClickSelection.cs
Click to select, click empty space to clear, hold Shift or Ctrl to add. Sits next to KeylinePointerHighlight and reuses its pick, so the scene casts one ray per frame rather than one for hover and another for the click. 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 look: both are held, and the arbiter shows the higher one. Move the pointer away and the selection stays.
Properties
Currently selected objects or groups, oldest first.
How many objects may be selected at once. 1 is classic single selection, 0 is no limit. Lowering it below the current count drops the oldest selections immediately.
Methods
Adds target to the selection and holds the selection state on it, dropping the oldest entries when the limit is reached. Ignored if the target is gone or already selected.
Drop a target from the selection.
More
The removal happens whether or not the target still exists, and the order matters: leaving a destroyed entry in the list would hang the eviction loop in Select, which deselects the oldest until there is room and would be handed the same dead entry forever. Only the two things that need a live object — popping its state and naming it in the event — are skipped.
Deselects every target, releasing the selection state on those still alive.
True when target is in the current selection.
Fields
Fires when an object joins the selection. For a group, the argument is the group's GameObject, not the clicked member.
Fires when an object leaves the selection: clicked again with the modifier, pushed out by Max Selection, or cleared.
class KeylineDamageFlash : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineDamageFlash.cs
Flashes the object when it takes a hit. Uses the Interior Fill pass rather than the rim. A rim flash is a hard read on a busy screen — it competes with every other outline in the frame — while a fill washes the whole body of the thing that was hit, which is the information the player actually needs. Drives the Interior slot directly instead of going through KeylineHighlight: the arbiter owns the rim, this owns the fill, and the two never contend. A hit landing on a selected object shows both.
Properties
public bool IsFlashing { get; }propertyTrue while a flash is playing.
public bool IsPlaying { get; }propertyAlias for IsFlashing. Every other timed recipe answers to IsPlaying, and code that drives several of them should not have to remember which one is different.
Whether a directional hit lights a half or fades from a point.
Falloff mode only: reach of a directional hit, in multiples of the object's size. Keep it at or below half a size or the falloff never lands on the model.
How soft the edge of a directional hit's falloff is.
Methods
public void Flash()methodPlay a normal flash at full strength. Wire this to a UnityEvent.
Play a flash scaled by strength — pass damage over max health and a scratch reads differently from a heavy hit for free.
Play the critical-hit flash, with no direction.
Flash from a point in world space: the fill is brightest where the hit landed and falls away from it. Cheaper than it sounds — the radial Gradient already measures distance from a world point, so the direction costs one vector and no extra draw. On anything larger than a crate it is the difference between knowing you were hit and knowing from where. For a travelling ring rather than a directional wash, use KeylineImpactWave; this stays a flash and keeps the same timing.
Critical hit, brightest at the point it landed.
public void Play()methodAlias for Flash. Same call, familiar name.
Alias for Flash. Same call, familiar name.
Alias for Flash. Same call, familiar name.
public void Stop()methodCut the flash short and put the slot back as it was. The other timed recipes have had this since they were written; this one did not, so a flash could only be waited out. Calling it while nothing is playing does nothing.
Part of IKeylinePassWriter: this recipe paints its own slot, so two of them on one pass would overwrite each other.
Fields
Fires on every flash, including a second hit landing while the first is still fading.
Fires when the object is back to its resting look: the flash faded, Stop was called, or the object was disabled.
enum KeylineDamageFlash.DirectionalShape
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineDamageFlash.cs
How a hit with a known direction paints the object.
class KeylineDistanceFade : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineDistanceFade.cs
Fades an outline out with distance from a camera, or from any object, and switches its passes off where it has faded away.
More about KeylineDistanceFade
Near stays as authored. Up to Fade Start the passes draw exactly as their profiles say; between Fade Start and Fade End their opacity, width or both drop along the curve; past Fade End they can be switched off altogether, which saves their draws rather than drawing them transparent. Invert turns it round, for markers that should only show from afar.
Cheap on hundreds of objects. The distance is measured on an interval, with each object starting at a random point in it so a scene full of pickups does not measure all at once, and nothing is written while the value stands still.
Writes go to the object's own copy of the profile, never to the shared asset, and are undone when the component is disabled. Another recipe that also sets opacity or width on the same pass — a Highlight state with its own opacity, say — takes turns with this one; fade the other value, or a different pass.
// Outlined up close, gone across the map.
var fade = pickup.AddComponent<KeylineDistanceFade>();
fade.SetReference(player); // measure from the player, not the camera
fade.FadeStart = 8f;
fade.FadeEnd = 15f;Properties
The camera the distance is measured from; null uses the main camera.
An object to measure from instead of a camera; null uses the camera.
Distance, in metres, up to which the passes draw as authored.
Distance at which the fade is complete.
The passes that fade.
public bool Invert { get; set; }propertyShow only from afar.
public float Visibility { get; }propertyHow much of the outline shows now, 0 to 1.
public float Distance { get; }propertyThe distance last measured, in metres.
public bool IsInRange { get; }propertyFalse once the outline has faded out completely.
Methods
Measure from camera; null uses the main camera.
Measure from reference rather than a camera; null goes back to the camera.
public void Snap()methodMeasure now and jump to the result, with no smoothing. For a camera cut or a teleport.
Part of IKeylinePassWriter: this recipe writes opacity and width into the passes it fades, so another recipe restoring the same slot would undo it.
Fields
Fires when the outline starts to show: it was hidden and is not any more.
Fires when the outline has faded out completely.
enum KeylineDistanceFade.MeasurePoint
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineDistanceFade.cs
Which point of the object the distance is measured to.
Values
PivotvalueThe object's own position. The cheapest.
BoundsCentervalueThe centre of the outline's bounds, for an object whose pivot is far from its middle.
enum KeylineDistanceFade.Passes
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineDistanceFade.cs
The passes a fade applies to.
Values
NonevalueNo pass.
MainOutlinevalueThe visible rim.
XRayOutlinevalueThe rim drawn where the object is hidden.
MainFillvalueThe visible body's fill.
XRayFillvalueThe hidden body's fill.
class KeylineFillGauge : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineFillGauge.cs
Turns the object itself into a gauge: health, charge, build progress, how full the barrel is. The mechanism is the Gradient style with two alphas. The filled end of the ramp is opaque and coloured, the empty end is transparent, and Gradient Offset slides the boundary between them along the sweep direction — so moving one float from -0.5 to 0.5 empties the object from top to bottom. Nothing is masked, clipped or re-meshed; it is one shader parameter. Worth knowing why this beats a bar over the head: the readout is on the thing it describes, at the thing's own scale, and it survives the camera moving. A world-space bar has to be billboarded, sorted, scaled with distance and hidden when off screen.
Properties
Seconds the drawn level takes to reach the value it was given. 0 snaps. Worth more than it looks: a health bar that jumps is a number changing, one that slides is a hit landing. The cost is that the bar lies for a moment, which is why it is a parameter and not a constant.
public float Value { get; set; }propertyTarget level, 0 to 1. Setting it animates the gauge towards the new value.
Level currently on screen, which lags Value while smoothing.
Whether the fill shifts towards LowColor as the value runs out. A level and a warning are two different pieces of information, and a bar that is merely shorter tells you the first while making you work out the second. Colour does it without being read.
Colour the fill reaches at zero.
Level at which the shift towards LowColor begins.
How soft the boundary is. 0 is a hard waterline.
public float Angle { get; set; }propertyDirection the gauge fills, in degrees. 0 fills bottom to top.
Colour of the filled part.
Colour of the empty part. Alpha zero makes the object simply run out.
Gradient Offset the gauge reads as empty.
Gradient Offset the gauge reads as full.
The offset currently being written, for calibrating by eye.
Methods
Set the level. Wire this to a UnityEvent that carries a 0..1 float.
Set the level from a pair of numbers — current health and max, say.
Jump straight to a level without the smoothing.
Part of IKeylinePassWriter: this recipe paints its own slot, so two of them on one pass would overwrite each other.
class KeylineGroupHighlight : MonoBehaviour, IKeylineHighlightTarget
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineGroupHighlight.cs
Highlights a squad as one thing: point at any member and the whole group answers. Two ways to draw it, and the difference is the interesting part. Separate gives every member its own outline — correct, and on a tight formation it turns into a thicket of contours where the bodies overlap. Merged puts the members in an KeylineMergeGroup, and the group is drawn as one silhouette with no seams between the bodies, which is what "these three are a unit" actually looks like. Merged is also cheaper in the way that matters: a merge group holds one stencil pair for the whole squad rather than one per member. The budget is 127 simultaneous independent outlines, so a merged squad costs one of them however many soldiers are in it.
Properties
Members currently in the group.
public bool Merged { get; set; }propertyWhether the group draws as one silhouette. Switching it at runtime allocates or releases the group's stencil pair.
Methods
Hold a state on every member, anonymously — the same terms as KeylineHighlight.Push.
Hold a state on every member on behalf of holder. The members keep it until every holder of the group's state has released it.
Release a state on every member, whoever holds it on the group — the same terms as KeylineHighlight.Pop.
Release holder's hold. The members let go only when nobody holds the state on the group any more.
True when anyone holds the state id on the group. Case-insensitive.
public void Clear()methodRelease everything the group is holding, whoever asked for it.
Take a member in at runtime. Reparenting is what actually moves it in and out of the merge group — membership there is the hierarchy, not this list. The controller notices the new parent by itself, so nothing else has to be told.
Drop a member and release whatever the group had it holding.
Rebuild the member list from the hierarchy on the next use.
More
A list the author filled in is left exactly as it is — that is the point of filling it in. Only a list this component gathered itself is gathered again.
class KeylineHighlight : MonoBehaviour, IKeylineHighlightTarget
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineHighlight.cs
Decides which look an object should be wearing when several things want to highlight it at once. Without an arbiter the last writer wins. Hover in, select, hover out — and the deselect that hover never performed removes the selection outline, because hover's "turn it off" ran after selection's "turn it on". That bug is not obvious in a two-script test scene and is very obvious in a shipped game. So nothing drives the controller directly. Each source holds a named state, states carry a priority, and the highest held state is the one on screen. Releasing it falls back to the next one down rather than to nothing. The states themselves are authored here, in the inspector, not in the scripts that hold them. What "Hover" looks like is an art decision and belongs next to the object, while the pointer script only knows that the pointer is over something.
Properties
public State Current { get; }propertyThe state currently on screen, or null when idle.
public string CurrentId { get; }propertyId of the state currently on screen; empty when idle.
The 3D controller, or null when this object is a sprite.
More
Kept at its original type on purpose. It shipped in 1.0.0 and scene scripts reach through it for members that have no sprite equivalent — cutout invalidation, the interior passes, the shader profile. Retyping it to the interface would have compiled here and broken there.
What this arbiter drives, whichever kind it is. Null only if the object carries no outline at all.
Methods
Hold a state. Holding one that is already held does nothing.
More
Held anonymously: one holder shared by every caller that does not name itself. See Push for holding as a named source.
Hold a state on behalf of holder. The state stays held until every holder has released it, so two scripts that both hold "Hover" no longer cancel each other. Holding again for the same holder does nothing.
More
| Parameter | |
|---|---|
id | State name. Case-insensitive. |
holder | Who holds it — usually the calling component. Null is the anonymous holder |
Release a state, whoever holds it. Releasing one that is not held does nothing.
More
The 1.2.0 meaning, kept: a script that clears a state a recipe set still clears it. To let go of only your own hold, use Pop with the holder you pushed as.
Release holder's hold on a state. The state goes only when nobody else holds it.
Hold or release in one call — the form most callers want. Releasing is Pop: the state goes whoever holds it.
Hold or release on behalf of holder in one call.
True when anyone holds the state id. Case-insensitive.
public void Refresh()methodRe-apply the winning state. For code that edited a state's fields in place — a proximity fade writing into the state's opacity, say — and needs the change to reach the screen without pretending the state changed.
public void Clear()methodRelease everything and go back to the idle look.
Look up an authored state so callers can read or tweak it.
Add a state at runtime, for looks that are not known at author time.
Fields
Raised when the winning state changes. The argument is the new state's id, or empty when the object goes idle.
class KeylineHighlight.State
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineHighlight.cs
One named look this object can wear, and how strongly it claims the object.
Fields
public string idfieldName scripts use to hold this state. Case-insensitive.
public int priorityfieldHigher wins when several states are held. Suggested: hover 10, proximity 20, selected 50, damage 100.
Optional profile for this state. Empty keeps the object's profile and applies only the overrides below, which is cheaper.
Replace the outline colour while this state is held.
public Color colorfieldOutline colour while this state wins and Override Color is on. Alpha still comes from the profile.
Multiplies the profile's width. 1 = unchanged.
public float opacityfieldMultiplies the profile's opacity. 1 = unchanged.
public bool showXRayfieldAlso switch the X-Ray pass on while this state is held.
Seconds to blend in and out. 0 snaps.
class KeylineImpactWave : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineImpactWave.cs
A ring of light spreading across the object from the point it was struck. The difference from KeylineDamageFlash is that this one carries a direction. A flash says the object was hit; a wave says where. On a large object — a boss, a vehicle, a wall — that is the difference between knowing something happened and knowing where to look, and it costs nothing extra to draw: the radial Gradient measures distance from a world point, so the point of impact is one vector. Speed is in metres per second rather than a duration, which is what makes it read as a wave. A fixed duration would cross a crate and a cathedral in the same time, so the cathedral's front would be visibly supersonic while the crate's crawls.
Properties
Which pass this wave is painted on.
public bool IsPlaying { get; }propertyTrue while a wave is travelling.
public float Speed { get; set; }propertyMetres per second the front travels at.
public float Reach { get; set; }propertyHow far the front gets before it stops, in metres.
Ring thickness as a fraction of the reach.
Hardness of the ring's edges. 0 is crisp, 1 is soft.
Seconds the wave takes to reach full strength.
Seconds the wave takes to disappear once it starts leaving.
Seconds at full strength between fading in and fading out.
public float FadeIn { get; set; }propertyobsoleteRenamed to FadeInSeconds.
Obsolete alias of FadeInSeconds.
public float FadeOut { get; set; }propertyobsoleteRenamed to FadeOutSeconds.
Obsolete alias of FadeOutSeconds.
public float Hold { get; set; }propertyobsoleteRenamed to HoldSeconds.
Obsolete alias of HoldSeconds.
Colour at the centre of the ring.
Colour at the ring's edges, and everywhere it has not reached. Alpha above zero washes the whole object for as long as the wave lasts.
public float Glow { get; set; }propertyBrightness at full strength; needs Bloom to glow. Negative values are treated as 0.
Methods
Send a wave out from a point in world space.
Send a wave out from where a raycast landed.
Send a wave out from where a 2D raycast landed.
More
The 3D overload above is the same convenience for the other physics world. Neither is required — both hand straight through to Emit — but a caller holding a hit should not have to unpack it.
public void Emit()methodSend a wave out from the object's own centre.
public void Play()methodAlias for Emit. Same call, familiar name.
Alias for Emit. Same call, familiar name.
Alias for Emit. Same call, familiar name.
Alias for Emit. Same call, familiar name.
public void Stop()methodCut the wave short and put the slot back as it was.
Part of IKeylinePassWriter: this recipe paints its own slot, so two of them on one pass would overwrite each other.
Fields
Fires on every Emit, including one that restarts a wave already travelling.
Fires when the wave ends: it ran its course, Stop was called, or the object was disabled.
class KeylineOccludedXRay : KeylineXRayWhenHidden
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineOccludedXRay.cs
Obsolete: Renamed to KeylineXRayWhenHidden in 1.2.0. This subclass keeps old code compiling
The 1.0.0 name for KeylineXRayWhenHidden, kept so that projects written against it keep compiling.
More about KeylineOccludedXRay
Why a subclass rather than a rename. The class was renamed in 1.2.0 because "occluded X-Ray" describes the mechanism and the new name describes what the component is for. Scenes and prefabs survive a rename on their own — Unity stores the script reference as the file's GUID, and the file kept its identity — but source code does not: a project that says GetComponent<KeylineOccludedXRay>() stops compiling the moment the package updates, which is a broken project rather than a changed one. An empty subclass costs nothing and makes the update additive.
It is the component, not a wrapper around one. Everything is inherited, so there is no second object to keep in step and no behaviour that can drift from the class it stands in for. A component of this type is a KeylineXRayWhenHidden to every GetComponent, cast and is check in the package and in your code.
Migrating. Rename the type in your scripts; nothing else about it changed. For a component already sitting on an object, switch its Script field to X-Ray When Hidden — the serialized fields carry over, because they are the same fields.
Hidden from Add Component, like every other internal or superseded component in the package: existing objects keep it, new ones should not be given it.
class KeylinePassWriterCheck
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylinePassWriter.cs
One-off check that no two slot-writing recipes on the same object claim the same pass. Editor and development builds only. It is a setup diagnostic, and a shipped game should not pay for it every time an object with recipes is enabled.
Methods
Warn once per object and pass when more than one recipe paints the same slot. Called from OnEnable by each writer. Only the first writer in the component order reports, so a clash produces one message naming everyone involved rather than one message per component saying the same thing.
class KeylinePointer
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylinePointer.cs
Pointer position and clicks, on whichever input backend the project is set to. Projects ship with the Input System, with the old Input Manager, or with both, and the two APIs do not overlap at all — UnityEngine.Input throws outright when the old manager is disabled. An asset that reads input has to handle every case or it is broken for a large share of buyers on first import, so all of the branching lives here rather than in each recipe.
Properties
public static Vector2 Position { get; }propertystaticPointer position in screen pixels. Zero when there is no pointer.
public static bool PrimaryPressedThisFrame { get; }propertystaticTrue on the frame the primary button goes down.
public static bool AdditiveModifierHeld { get; }propertystaticTrue while a modifier that means "add to the selection" is held.
Methods
public static bool OverUI()methodstaticTrue when the pointer is over a UI element that should swallow the click. Without this check the world highlight fires through inventory panels and dialogue boxes, which is the single most common complaint about hover-highlight code.
class KeylinePointerHighlight : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylinePointerHighlight.cs
Highlights whatever the pointer is over. One of these in the scene, not one per object: a single raycast per frame answers the question for everything, whereas a per-object script would cast once per object and cost a hundred raycasts to find the one thing under the cursor. The object under the pointer needs a collider and an KeylineHighlight. The collider is what makes this cheap — the physics scene already indexes them, so nothing here has to walk the hierarchy.
Properties
What the pointer is over — one object or a whole group. Null when nothing.
Name of what the pointer is over, for readouts. Empty when nothing.
Methods
What is under the pointer right now. Public so selection can reuse the pick.
class KeylineProximityHighlight : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineProximityHighlight.cs
Highlights the object while a target is within range, fading it in as the target closes. The difference from the trigger version is the fade. A trigger is binary and reads as a pop; a distance gives you something to interpolate, so a room full of pickups reveals itself gradually instead of flashing on at an invisible boundary. The check runs on an interval rather than every frame. A distance test is cheap, but a hundred pickups times sixty frames is six thousand of them per second for information that changes at walking speed.
Properties
Distance at which the highlight is fully on.
Distance at which the highlight switches off.
What the distance is measured to. Assigning it overrides the tag lookup, which is what spawned content wants: the player may not exist yet when this component starts, and a tag search that comes back empty is silent.
class KeylineReveal : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineReveal.cs
Materialises an object: the outline draws it before the mesh exists, then hands over. The sequence is the point. The body is hidden and a fill climbs the silhouette; when it reaches the top the outline flares, and under that flare the mesh switches on. The flare is not decoration — it is what covers the substitution, the same trick a stage magician uses. Without it the swap is a visible pop, because a fill and a lit mesh never quite match. Nothing here is masked or dissolved. The climb is the gradient's offset, so it costs one float per frame and works on any mesh, skinned included, with no second material and no alpha-clip variant.
Properties
public bool IsPlaying { get; }propertyTrue while a reveal or a dissolve is running.
Seconds the fill takes to climb from empty to full. At least 0.01.
Seconds the flare lasts once the fill is full; the body appears at its peak.
How bright the flare gets, 1 or more. Needs Bloom to read as a flash.
Colour of the fill as it climbs.
How soft the fill line is, 0 to 1. 0 is a hard waterline.
Where the climb starts, in the ramp's own coordinate (−1 to 1). Set per model so the fill is just out of sight at the start.
Where the climb ends, in the ramp's own coordinate (−1 to 1). Set per model so the fill just covers the body at the end.
Methods
public void Play()methodHide the mesh and play the arrival.
Play the arrival backwards: the outline flares, the mesh goes under it, the fill drains away. The flare comes first here and last in a reveal, because in both cases its job is to be on screen at the moment the mesh changes state. Put it at the end of a dissolve and it lights up an object that is already gone.
public void Complete()methodStop wherever it is and leave the object fully present.
public void Stop()methodAlias for Complete, so that code driving several timed recipes at once can call Stop on all of them. Note what it does here, because it is not what Stop does elsewhere: Reveal ends with the object present, and a cancel that left it half-dissolved would leave the scene in a state the caller never asked for. Ending it early therefore means finishing it, and Revealed fires exactly as it would have on its own.
Part of IKeylinePassWriter: this recipe paints its own slot, so two of them on one pass would overwrite each other.
Fields
Raised when the mesh has appeared and the fill has gone.
Raised when a dissolve has finished and the object is gone.
Fires whenever a run begins, in either direction. Use Revealed or Dissolved when the direction matters.
Fires when a run ends, in either direction, alongside Revealed or Dissolved.
class KeylineScanWave : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineScanWave.cs
One front of light crossing a whole scene, in step, through every object it touches. This is the recipe the asset's Style Space work was really for. The gradient's radial and planar shapes measure position in absolute world space, so a hundred objects asked the same question about the same origin give the same answer — a sphere expanding from a point or a plane sweeping along an axis, unbroken across the level. Nothing else here can do that: a per-object coordinate restarts on every mesh, and the wave falls apart into a hundred little waves. Lives on the scene, not on the objects. One component owns the front, the objects only have to be outlined.
Properties
public bool IsPlaying { get; }propertyTrue while a front is travelling.
public float Distance { get; }propertyHow far the front has travelled, in metres.
public float Speed { get; set; }propertyMetres per second the front travels at. Minimum 0.01.
public float Range { get; set; }propertyHow far the front travels before it stops, in metres. Minimum 0.1.
Thickness of the front, in metres. Minimum 0.01.
Hardness of the front's edges: 0 is a crisp scan line, 1 a soft glow.
Whether the front is a growing sphere or a travelling plane.
Plane only: the direction the front faces and travels. A zero vector is replaced by Vector3.up.
Colour at the centre of the front.
Colour at the front's edges, and everywhere it has not reached. Alpha above zero tints the whole object for as long as the wave exists.
public float Glow { get; set; }propertyBrightness at the front; needs Bloom to glow. Negative values are treated as 0.
Methods
public void Ping()methodSend a front out from this object's position.
Send a front out from a point in world space.
public void Play()methodAlias for Ping. Same call, familiar name.
Alias for Ping. Same call, familiar name.
public void Stop()methodStop the front and put every object it touched back as it was.
Rebuild the target list from the scene on the next ping.
Fields
Fires on every Ping, including one that restarts a front already travelling.
Fires when the front ends: it passed its range, Stop was called, or the object was disabled.
enum KeylineScanWave.WaveShape
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineScanWave.cs
The shape of the front.
class KeylineSlotRestore
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylinePassWriter.cs
Putting a forked slot back the way the shared profile has it, style included.
More about KeylineSlotRestore
Why the style needs saying separately. The five slot-writing recipes all take the slot over the same way — through the controller, which notices that Gradient needs a different set of mirrors than Frost, Halo or Double and rebuilds them. They all gave it back the other way, with a value copy, and a value copy cannot rebuild anything: it does not raise the profile's Changed event, and the appearance refresh that follows only re-applies what is already built. So a Frost profile came out of an effect still called Frost, with its shell mirrors destroyed, drawing a single flat band until something else happened to rebuild it.
And why the order is what it is. SetStyle returns early when the profile already holds the style being asked for — so it has to run before the copy, while the slot still holds the effect's style and the difference is visible. The copy then restores every other field onto mirrors that already have the right shape, and the caller's own appearance refresh puts them on screen.
One place rather than five, because five copies of this is how the five drifted from their own take-over path to begin with.
Public because the trap is not Keyline's alone. Anything that borrows a pass, writes a temporary look into it and later gives it back hits the same ordering — a custom highlight, a cutscene, a save system restoring what it captured. The rule is unguessable and the failure is silent, so the answer belongs in the package rather than in five copies of a comment.
Methods
class KeylineTriggerHighlight : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineTriggerHighlight.cs
Highlights the object while something is inside its trigger. The zero-code version of "show me what I can interact with": put a trigger collider on the pickup, tag the player, done. No raycast, no per-frame distance check — the physics engine already tracks overlaps and only calls back when one starts or ends. Counts overlaps rather than tracking a single occupant, so two players in the zone and one leaving does not switch the highlight off for the one still standing there.
Properties
The collider that defines the zone. Assigning a collider on a child object wires up the forwarding this component needs to hear about it at all.
The 2D collider defining the zone, wired the same way as the 3D one.
class KeylineTriggerRelay : MonoBehaviour
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineTriggerRelay.cs
Forwards trigger callbacks from a child collider up to the component that cares. Unity delivers OnTriggerEnter to the GameObject holding the collider, and — only if there is one — to the attached Rigidbody's GameObject. A zone modelled as a child object, which is the normal way to do it, therefore never reaches a component sitting on the parent. Nothing errors; the callbacks simply go somewhere nobody is listening. Added automatically by KeylineTriggerHighlight. Not meant to be added by hand, hence the empty Add Component path.
class KeylineXRayWhenHidden : MonoBehaviour, IKeylinePassWriter
FourSsets.Keyline.Recipes · Runtime/Recipes/KeylineXRayWhenHidden.cs
Switches the X-Ray pass on only while the object is actually hidden behind something. Left on permanently, a through-walls outline stops meaning anything: it is on when the objective is in plain sight and on when it is behind a building, so the player learns to ignore it. Tying it to real occlusion turns it back into information — the marker appears exactly when the thing it marks disappears. Occlusion is tested on an interval, and how it is tested depends on what the object is. A mesh gets a line cast from the camera, because in 3D an occluder is a collider and a ray is what finds one. A sprite gets a question put to Keyline's own occluder registry instead: in 2D an occluder is a sprite somebody declared — through Keyline Occluder 2D, or by naming its sorting layer in Keyline Occluder Layers 2D — and it carries no collider at all, so a physics cast comes back empty in a scene full of them. The interval is what keeps either affordable for a squad of objects rather than one. The appearance is faded rather than switched. Occlusion is a binary fact, but a marker that pops in at full strength reads as a glitch — the eye catches the discontinuity before it reads the meaning. A fifth of a second is enough to turn it into a thing that arrived.
More about KeylineXRayWhenHidden
What this adds over the depth test. The X-Ray pass already draws nothing while the object is in the open — its depth test sees to that. What the depth test cannot do is remember: it has no fade, so the marker pops; no hysteresis, so it flickers as the player walks along a railing; no notion of distance, so a far objective is treated as visible; and no off switch, so a squad of markers pays for a pass each whether or not any of them is hidden. This component is the policy on top of that geometry, which is why it is a recipe and not part of the pass.
Properties
public bool IsOccluded { get; }propertyTrue while the object is judged to be behind something.
public float Blend { get; }propertyHow far the marker currently is into its fade: 0 invisible, 1 fully shown.
Whether the X-Ray interior fill follows the rim. A rim alone is the honest default — it says where the object is without claiming any of the screen behind it. A filled silhouette is louder and reads at a distance, which is what an objective marker wants and what a dozen simultaneous ones do not. While this is on the component owns the fill pass and switches it off with the rim.
Seconds the marker takes to fade in. 0 switches instantly.
Seconds the marker takes to fade out. 0 switches instantly.
Methods
Re-read the Opacity the profiles are authored with. The fade multiplies that number, so anything editing the X-Ray profiles at runtime — an options screen, the demo's own panel — has to say so, or the next fade would scale the value it left behind and treat it as the new full strength. Ignored mid-fade: the profiles hold a scaled value then, and nothing can tell that apart from an edit.
Part of IKeylinePassWriter: this recipe forks and scales the X-Ray profiles itself, so another slot-writing recipe on the same pass would fight it — whichever restores first erases the other's fade.
Project and features
Project-wide switches.
class KeylineFeatures
FourSsets.Keyline · Runtime/KeylineFeatures.cs
Which parts of Keyline this project uses — the single place every other part of the package asks.
More about KeylineFeatures
Everything is on unless somebody says otherwise, and that is the behaviour of 1.2.0. A project that never opens the Features page, and a player build that carries no manifest, both answer yes to every question here.
Nothing a buyer authored is ever changed by this. A switched-off feature is hidden and ignored — a Fire profile draws as Solid, an X-Ray slot is skipped — while the values stay exactly where they were, so switching the feature back on brings every one of them back. That is also what keeps a scene saved under a fuller set of features safe to open under a smaller one.
Two masks, and the difference matters. RawMask is what the buyer ticked. EffectiveMask is that, with the dependency rules applied: a child whose parent is off is off, and a feature another one cannot work without is on. The raw answer is kept so that switching a parent back on restores its children as they were ticked rather than as they were forced.
Filled in the Editor by the settings asset, and in a player by the runtime manifest the build writes — see KeylineRuntimeManifest.
// Skip work for a feature the project has switched off.
if (KeylineFeatures.Allows(KeylineFeature.XRay))
{
outline.XRayOutlineEnabled = true;
}Properties
public static ulong RawMask { get; }propertystaticWhat was ticked, before dependencies were applied.
public static ulong EffectiveMask { get; }propertystaticWhat is actually in force.
public static bool IsEverything { get; }propertystaticWhether every feature is on — the shipped behaviour.
public static bool AllowsXRayOverlay { get; }propertystaticThe X-Ray Overlay pass needs both switches it belongs to.
Methods
Whether a feature is in force in this project.
Whether a style may be drawn. Solid always may: it is the fallback every other answer lands on, so it cannot itself be taken away.
The feature that stands for a style, or null for Solid.
The style a style feature stands for. Solid for anything that is not one.
Whether a feature is one of the style switches.
The name a feature goes by on the Features page and in every message.
public static ulong Bit(KeylineFeature feature)methodstaticThe mask bit for a feature.
public static void SetRawMask(ulong raw)methodstaticHand over a new ticked set. The dependency rules are applied here, once, and nothing downstream ever sees a set that breaks them.
More
Idempotent: handing over the set already in force raises nothing, so the Editor can push on every load without rebuilding a scene that did not change.
Features that must be on for this one to be on. Its own tick is kept while they are off, and honoured again when they come back.
Features this one cannot work without and therefore turns on. Different from Requires: nothing here is a parent, it is machinery borrowed from another switch.
public static ulong Resolve(ulong raw)methodstaticThe ticked set with every rule applied.
More
Order matters and is fixed: platforms first (at least one must stand, so there is always something to outline), then parents, then the features borrowed by what survived, then parents once more for what was borrowed — a borrowed feature still needs its own parent, and Clip At Intersections has nothing to clip on a sprite.
What a raw mask means for a project whose saved choice predates some features.
More
A feature added in a later version is not in anybody's saved choice. Reading its absent bit as "off" would switch it off in every project on the day it ships, so the saved set records which features it has an opinion about, and everything outside that set is on.
Events
public static event Action ChangedeventstaticRaised when the effective set changes. Every outline in the loaded scenes rebuilds from it, and every system that subscribes per frame re-reads whether it is wanted.
Fields
One past the highest defined feature.
public static readonly ulong EverythingfieldstaticEvery defined feature, on.
enum KeylineFeature
FourSsets.Keyline · Runtime/KeylineFeatures.cs
A part of Keyline a project can switch off in Project Settings ▸ Keyline ▸ Features.
More about KeylineFeature
The numbers are the stored bit positions, so they are appended, never reordered. A project's choice is saved as a 64-bit mask; renumbering an entry would silently switch a different feature off in every project that saved the old order.
What is not here is deliberate. The core — Keyline Outline 3D or 2D, the Main Outline pass, the Solid style, a hull grown along normals — cannot be switched off, and neither can API that costs nothing until it is called (AdoptChild, per-part pass switches, Configure). A switch is only offered where turning it off removes something real: shader variants, draw calls, per-frame work, editor hooks, or a block of the inspector.
Values
Outlines3DvalueKeyline Outline 3D, for meshes.
Outlines2DvalueKeyline Outline 2D, for sprites.
SpriteOccludersvalueKeyline Occluder 2D and Occluder Layers 2D: sprites that hide outlines so X-Ray shows.
MainFillvalueThe Main Fill pass, which tints the visible body.
XRayvalueThe X-Ray Outline pass, the rim drawn where the object is hidden.
XRayFillvalueThe X-Ray Fill pass, which tints the hidden body.
OverlayPassesvalueMain Overlay and X-Ray Overlay, a second draw of the outline on meshes.
LayerFilteringvalueThe Layer Filtering pipeline pass that tests outlines against chosen layers.
XRaySeesThroughvalueX-Ray Sees Through: layers that reveal the X-Ray when they hide the object.
MainSeesThroughvalueMain Sees Through: layers the Main Outline keeps drawing through.
MainContactDepthvalueMain Contact Depth: the Main Outline stays whole where the object meets a surface.
SoftEdgevalueThe Soft Edge pipeline pass: Edge Softness and world-space halos.
HaloOcclusionvalueHalo Occlusion: whether the scene in front cuts a soft rim.
SheetsvalueSheets: the flat parts of a mesh — cards, leaves, blades, paper — grown in their own plane and given a shell. Off, every part grows along its normals.
AlphaCutout3DvalueRims cut from a mesh texture's alpha.
ClipAtIntersectionsvalueKeeps a rim off its own object where it passes through other geometry.
MeshBakingvalueBake for Runtime: outline meshes saved as assets.
StyleNeonvalueThe Neon style.
StyleHalovalueThe Halo style.
StyleDoublevalueThe Double style.
StyleGradientvalueThe Gradient style.
StyleRainbowvalueThe Rainbow style.
StylePatternvalueThe Pattern style.
StyleSketchvalueThe Sketch style.
StyleElectricvalueThe Electric style.
StyleFrostvalueThe Frost style.
StyleLavavalueThe Lava style.
StyleSmokevalueThe Smoke style.
StyleCrystalvalueThe Crystal style.
StyleWatervalueThe Water style.
StyleFirevalueThe Fire style.
PulsevalueA rim that swells and brightens in a loop.
BlendModesvalueBlend modes other than Default.
MobileSolidProfilesvalueMobile Solid profiles, drawn by the cheaper Mobile Solid shader.
MergingvalueOverlapping outlines merging into one silhouette, and Merge Group.
CustomDrawConfigvalueKeyline Draw Config: overrides for queue, depth, culling and stencil of 3D draws.
RecipesvalueThe ready-made behaviours under Add Component ▸ Keyline ▸ Recipes.
LodGroupsvalueOne outline per LOD Group, built from one LOD level and culled with the group.
class KeylineRuntimeFlags
FourSsets.Keyline · Runtime/KeylineRuntimeFlags.cs
Project-wide choices that change what the runtime does, as the runtime sees them.
More about KeylineRuntimeFlags
Properties and no storage, like KeylineHdrpPassFlags beside it, and for the same reason: the values are kept in ProjectSettings/KeylineSettings.asset, which a player build cannot read and the runtime assembly cannot reference. Who fills them depends on where the code is running, and both answers arrive before anything reads them — the settings page pushes them in the editor, KeylineRuntimeManifest carries them into a player.
The defaults here are the shipped behaviour. A project that never opens the settings page, and a build with no manifest because nothing was changed, both run on these values — so they have to be the ones Keyline is documented to have.
Properties
public static bool KeepMirrorsWhileHidden { get; set; }propertystaticWhether hiding an outline keeps its mirrors so showing it again costs nothing.
More
On, an outline switched off leaves its generated objects in place with their renderers disabled: switching it back on is a flag per renderer. Off, they are destroyed and the next show rebuilds them — less memory held by objects nobody is looking at, paid for at the moment somebody looks.
Disabling counts as hiding, in Play mode. A component disabled, or an object deactivated — what an object pool does — keeps its generated objects the same way, and shows them again on enable if nothing they were built from has changed in between: the component's settings, its profiles' structure, the renderers it outlines and their meshes and materials. Anything changed and it builds as it always did. An outline that passes its look on to child outlines, or has adopted or been adopted, always rebuilds. The stencil pair is given back on disable either way, so a pool full of disabled objects does not exhaust the scene's pairs.
On by default because the hide/show cycle is the common one: a hover highlight, a selection, a ping, an object coming back from a pool. Turn it off when many outlines are hidden or disabled at once and stay that way.
public static bool ReportApiMisuse { get; set; }propertystaticWhether runtime code writing somewhere it should not be is reported.
More
Two mistakes, both silent without this. A look property written while the pass's slot is empty goes to a scratch object and changes nothing. A look property written while the slot holds a shared profile asset and Instance Override is off changes every object using that profile — and in the editor the asset is modified on disk, so the change outlives play mode.
Each is reported once per outline. The Editor and development builds only; a release build never checks, and the second test needs the asset database, so it runs in the Editor alone.
class KeylineEditorBehaviour
FourSsets.Keyline · Runtime/KeylineEditorBehaviour.cs
The editor conveniences Keyline performs on its own, as plain flags the runtime half can read.
More about KeylineEditorBehaviour
Why the values live here and not with the settings that own them. The things these switch are implemented in the runtime assembly — the mirror tag, the two outline components — inside their own editor-only blocks. A runtime assembly cannot reference an editor one, so the settings asset cannot be read from where the behaviour is. Statics can be, and pushing three booleans on load is cheaper than the plumbing any other arrangement would need.
The Editor half owns persistence entirely: KeylineSettings writes these on load and on every change.
The values written here are the settings page's defaults, not bool's. The push happens on load, long before anything can read them, so these initialisers only ever cover the gap between the assembly loading and that push — but a default written here that disagreed with the settings asset would be a lie waiting for somebody to read it.
Compiled out of player builds completely. None of it describes anything that happens in a build.
Fields
public static bool BlockMirrorPickingfieldstaticTake generated mirrors out of Scene view picking.
More
A mirror sits on top of the object it outlines and is usually slightly larger, so a click in the Scene view lands on the mirror. Drag it and the outline is somewhere the object is not, until the next rebuild throws the edit away.
Off — the default — leaves picking alone, which is what the package always did. On is worth having the moment somebody drags a mirror by accident.
Rebuild an outline in the Editor when the renderers under it change.
More
Adding a child to an outlined object should give it an outline without anybody pressing anything. The check is a comparison, not a rebuild — see RebuildIfSourceSetChanged — so an unrelated hierarchy change costs one walk of an already-cached list.
On is the default. An outline that covers an object's renderers is expected to cover the one just added to it, and the alternative — a child that appears with no outline and no explanation — reads as a bug rather than as a switch left off.
Enumerations
Values the rest of the API takes.
enum KeylineBlendFold
FourSsets.Keyline · Runtime/KeylineStyle.cs
What the fragment has to fade towards where it draws nothing, for the chosen blend to leave the frame alone there.
More about KeylineBlendFold
Every blend has a value that changes nothing — its identity. Alpha's is carried by the alpha channel and needs no help. Adding, screening and taking away all leave the frame as it was when the source is black. Multiplying and darkening leave it alone when the source is white. Get this wrong and the uncovered part of the quad is not invisible: under Multiply a black identity would multiply the whole quad to black.
Values
NonevalueCoverage rides in the alpha channel. Alpha blending only.
TowardsBlackvalueFade to black where there is no coverage. Additive, Screen, Subtract, Lighten.
TowardsWhitevalueFade to white where there is no coverage. Multiply, Darken.
enum KeylineBlendMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
How the outline is composited over what is behind it.
More about KeylineBlendMode
Until this existed the style decided and could not be argued with: Neon, Halo, Electric and Rainbow were composited additively and everything else with alpha. That is the right default — those four are light, and light adds — but as a law it made Opacity a lie on four styles out of ten. Additive compositing cannot cover what is behind it at all, so Opacity there only dimmed the colour and could never make the outline solid.
Every mode below is a fixed-function blend, so none of them costs a pass, a texture or a variant — only different numbers in the same two state fields and the blend op.
Values
DefaultvalueThe style decides, as it always has. Neon, Halo, Electric and Rainbow add; the rest blend.
AlphavalueOrdinary transparency. Opacity means what it says: at 1 the outline hides what is behind it. Choose this to make a light style solid.
AdditivevalueLight. The colour is added to the frame, so the background always shows through and brightness lives in the colour rather than in Opacity. Bright over dark; over a light background it clips to white and the shape is lost.
ScreenvalueLight that cannot blow out. Approaches white asymptotically instead of clipping at it, so a glow keeps its shape over a bright background where Additive turns into a patch. The gentler of the two glows, and usually the better one over unknown scenery.
MultiplyvalueInk. The frame is multiplied by the outline's colour, so the outline darkens and tints rather than covering: a dark contour, a coloured shadow, a stamped line. White leaves the frame untouched, which is why Opacity here reads as how far towards the colour to take it.
SubtractvalueThe colour is taken out of the frame. A hole cut in the light: the brighter the colour, the darker the result, and a white outline drives what it crosses to black.
DarkenvalueWhichever is darker, channel by channel, the frame or the outline. Never brightens anything, and unlike Multiply it leaves what is already darker than the outline completely alone.
LightenvalueWhichever is brighter, channel by channel. A glow that never exceeds its own colour however many outlines overlap, where Additive would keep climbing.
enum KeylineCornerTopology
FourSsets.Keyline · Runtime/KeylineOutlineController.cs
How hard corners of the outline hull are built.
Values
RoundedvalueBuild a bevel at each hard corner. A Sheet Solidify shell's rim is rounded from lid to lid, and its rounded corners become domes.
SimplevalueKeep one vertex at each corner, mitered up to Sharp Limit. Only the open rim of a double-sided surface, where front meets back, gets a rim built round it: square, with its corners cut.
enum KeylineDrawCompare
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Depth or stencil comparison for a draw stage. Values match Rendering.CompareFunction.
Values
DisabledvalueTest disabled.
NevervalueNever passes.
LessvaluePasses when less than the stored value.
EqualvaluePasses when equal to the stored value.
LessEqualvaluePasses when less than or equal to the stored value.
GreatervaluePasses when greater than the stored value.
NotEqualvaluePasses when not equal to the stored value.
GreaterEqualvaluePasses when greater than or equal to the stored value.
AlwaysvalueAlways passes.
enum KeylineDrawCull
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Face culling for a draw stage.
Values
UseProfilevalueUses the outline's own value, normally Face Cull from the Style Profile.
FrontvalueCulls front faces.
BackvalueCulls back faces.
OffvalueNo culling.
enum KeylineDrawLayer
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Position of a draw within its queue family; see KeylineCustomDrawConfig.LayerOffset.
Values
SelfOccludevalueThe self-occlude stamp.
FillvalueThe interior fill.
MaskvalueThe hollow mask.
RimvalueThe outline rim.
GapvalueDouble's gap mask and Halo's inner layers.
ExtravalueExtra hulls: Frost's shell and Double's outer ring.
ClipClaimvalueClip At Intersections' claim, between the fill and the rim.
enum KeylineDrawRefMode
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Stencil reference value a draw stage uses.
Values
OwnClaimvalueThe outline's own claim value.
HollowBitvalueThe shared X-Ray hollow bit (1).
ClaimPlusHollowvalueThe outline's claim value with the hollow bit set.
enum KeylineDrawStageId
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Identifies one 3D Keyline draw whose depth, cull and stencil state a KeylineCustomDrawConfig defines.
Values
MainOutlinevalueThe visible (Main) outline rim.
MainInteriorFillvalueThe Main interior fill.
The Main interior fill when Single Layer is on; claims each pixel so it is painted once.
MainMaskvalueStencil-only stamp of the body that keeps the Main rim hollow.
MainSelfOccludevalueStencil-only self-occlude stamp drawn with the Main family.
XRayOutlinevalueThe X-Ray rim, drawn where the object is hidden.
XRayInteriorFillvalueThe X-Ray interior fill.
XRayMaskvalueStencil-only stamp of the hidden body that keeps the X-Ray rim hollow.
XRaySelfOccludevalueStencil-only stamp of the visible body that stops X-Ray drawing over it.
MainOverlayOutlinevalueThe rim of the Main cutout overlay pass.
MainOverlayMaskvalueThe hollow mask of the Main cutout overlay pass.
XRayOverlayOutlinevalueThe rim of the X-Ray cutout overlay pass.
XRayOverlayMaskvalueThe hollow mask of the X-Ray cutout overlay pass.
GapMaskvalueStencil mask that keeps the gap between Double's rings empty.
ExtraHullvalueAdditional hulls: Halo layers, Frost's shell and Double's outer ring.
MainClipClaimvalueClaims the covered part of the silhouette for Clip At Intersections.
MainClipReleasevalueReleases the Clip At Intersections claim before the X-Ray family draws.
enum KeylineDrawStencilMask
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Stencil read or write mask for a draw stage.
Values
NonevalueNo bits (0).
HollowBitvalueBit 0 only, the shared X-Ray hollow bit.
ClaimBitsvalueBits 1-7, the per-outline claim bits.
HollowAndClaimvalueAll eight bits.
CustomvalueThe stage's custom mask value.
enum KeylineDrawStencilOp
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Stencil operation applied when a draw stage passes. Values match Rendering.StencilOp.
Values
KeepvalueKeeps the current stencil value.
ZerovalueSets the stencil value to 0.
ReplacevalueWrites the reference value.
IncrementSaturatevalueIncrements the value, clamping at the maximum.
DecrementSaturatevalueDecrements the value, clamping at 0.
InvertvalueInverts all bits.
IncrementWrapvalueIncrements the value, wrapping to 0 past the maximum.
DecrementWrapvalueDecrements the value, wrapping to the maximum below 0.
enum KeylineExpandCenterMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
Obsolete: Obsolete.
Object-space origin Planar grew from before 1.3.0. Nothing reads it since.
Values
MeshBoundsvalueMesh bounds center (default).
ObjectPivotvalueObject pivot (local origin).
CustomvalueCustom object-space point.
enum KeylineExpandMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
Obsolete: Obsolete.
How width expanded the outline geometry before 1.3.0. Since then every hull is built as Mixed was: solid parts grow along their normals, sheets in their own plane.
enum KeylineFaceCull
FourSsets.Keyline · Runtime/KeylineStyle.cs
Which mesh faces draw the outline / stencil mask (Unity Cull).
enum KeylineGradientRadiusMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
How a radial gradient's radius is measured.
enum KeylineGradientShape
FourSsets.Keyline · Runtime/KeylineStyle.cs
How the Gradient style measures its position across the object. Linear projects onto an axis: the classic top-to-bottom ramp. Radial measures distance from a point in world space, which is what makes a shared wave possible — every object answers the same question about the same origin, so the front crosses a whole scene in step instead of restarting on each mesh.
Values
LinearvalueRamp along a direction, set by Gradient Angle.
RadialvalueRamp outwards from Gradient Center, spanning Gradient Radius.
PlanarvalueRamp across a plane through Gradient Center, facing Gradient Axis. The scene-wide counterpart to Linear. Linear measures in the object's own space, so a stripe restarts on every mesh; Planar measures against a plane in absolute world space, so one front crosses a whole level in step — a wall of light rather than a per-object sweep.
enum KeylineHaloOcclusion
FourSsets.Keyline · Runtime/KeylineStyle.cs
Whether a soft edge obeys the same depth rule its rim is drawn with, or ignores depth entirely.
More about KeylineHaloOcclusion
The rim is depth-tested; its blur is not. Rims are drawn against a copy of the camera's depth, so a Main rim never appears behind a wall and an X-Ray rim never appears in front of one. What has no depth is the blur — a full-screen filter that spreads colour outward across whatever line the depth test just drew. Both families get an artefact out of it, and they are mirror images:
- Main — an object half behind a wall casts its glow onto the wall's face, where the rim itself was correctly cut away.
- X-Ray — the glow of a rim seen through a wall smears past the wall's edge onto open space, where the rim itself correctly drew nothing.
So this is one rule, not two features. The halo survives where its rim would have been allowed to draw: Main where its emitter is in front of the scene, X-Ray where its emitter is behind it. The comparison is the same, and only its direction follows the family — which is why the setting reads the same on both profiles and needs no exception.
An earlier version of this 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 halo on the far side of the wall rather than spilling off it.
Both answers are legitimate, and Match Rim is the default because it is the one that matches the rim. A glow that ignores depth reads as light, which some looks want — but a soft edge whose hard version is depth-tested and whose blur is not is not a style choice, it is a discrepancy, and defaulting to it means every buyer meets the artefact before they meet the setting that explains it.
The numbering carries that default, and it has to. Unity gives a field absent from a serialized asset the enum's zero, not the C# initialiser — so a profile authored before this field existed takes whichever value is 0. Putting Match Rim there is what makes the default true for profiles that already exist as well as for new ones. Renumbering is also why this had to be settled before 1.2.0 ships: after that, moving the zero would silently invert every profile in every buyer's project.
Values
MatchRimvalueThe soft edge is cut to the same depth rule as the rim that produced it. The default.
More
Costs one screen-sized two-channel 16-bit target and a second, cheaper filter chain beside the blur, for the rims that use it — which, being the default, now means any rim with Edge Softness above zero. A scene with no soft rim at all still allocates nothing.
IgnoreDepthvalueThe soft edge draws wherever the blur reaches, ignoring depth. What Keyline did before 1.2.0, and still the right answer when the glow is meant to read as light rather than as a marker attached to a silhouette.
enum KeylineMirrorLayer
FourSsets.Keyline · Runtime/KeylineMirrorLayer.cs
Which layer the objects an outline draws with are put on.
More about KeylineMirrorLayer
An outline is drawn by copies of the outlined renderers (mirrors). A camera decides what to draw by layer, so a mirror on a different layer from its object is drawn by a different set of cameras: a weapon drawn by its own camera had its outline drawn by the main camera, and a camera without the Default layer lost every outline.
Values
FixedvalueEvery mirror on layer 0 (Unity's Default layer), whatever layer its object is on. How 1.2.0 drew, and what components saved before 1.3.0 keep.
MatchRenderervalueEach mirror on the layer, and with the rendering layers, of the renderer it copies, so cameras and layer-filtered render features treat the outline like the object.
enum KeylineMixedDetectMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
How the hull decides sheet (flat) vs volume faces.
enum KeylinePassKind
FourSsets.Keyline · Runtime/KeylineOutlineController.Passes.cs
An outline draw pass owned by a single KeylineOutlineController — no companion components.
More about KeylinePassKind
The last two arrived later, and only at the API surface. A Cutout Overlay is still, inside the controller, a Primary or an X-Ray pass carrying a flag; that is what the X-Ray family tests, the stencil pairing and the draw-order rules read, and it is not going to change. What these two names buy is the ability to address those passes — a save system, a HUD, an editor tool or anything else written generically over the passes had to carry a fifth and sixth special case for them, and every copy of that is a place to forget one.
Mesh only. A sprite has four mirrors and no overlay pass to address. The sprite component answers for these two the way it answers for an empty slot — nothing to read, nothing to switch — rather than quietly treating them as the visible rim.
Values are fixed: they are serialised on recipe components, so the four that were here first keep the numbers they had.
Values
MainOutlinevalueThe rim drawn where the object is visible.
XRayOutlinevalueThe rim drawn where the object is hidden behind something.
MainFillvalueThe tint over the visible body.
XRayFillvalueThe tint over the hidden body.
MainCutoutOverlayvalueThe Cutout Overlay drawn with the visible rim. Mesh only.
XRayCutoutOverlayvalueThe Cutout Overlay drawn with the X-Ray rim. Mesh only.
enum KeylineProfileFor
FourSsets.Keyline · Runtime/KeylineStyle.cs
What a profile is drawn on, which decides the controls it needs.
More about KeylineProfileFor
A mesh and a sprite read overlapping but different halves of a profile. Face culling and the overlap fence are mesh questions a quad has no use for; rim sampling and rim softness need an alpha the mesh path does not have. Showing all of it at once means most of what is on screen does nothing, and there is no way to tell which part.
enum KeylineRainbowMotion
FourSsets.Keyline · Runtime/KeylineStyle.cs
How Rainbow bands sweep. Independent of KeylineStyleSpace.
enum KeylineSdfFieldMode
FourSsets.Keyline · Runtime/KeylineSdfNaming.cs
How the field measures distance, which decides what kind of edge can be drawn from it.
More about KeylineSdfFieldMode
Why this is baked and not a draw-time switch. The rounding is in the numbers, not in how they are read. A smooth field holds Euclidean distances, and the shortest way to a corner really is round, so thresholding one hard gives a hard edge with rounded corners — the wrong answer arrived at crisply. A pixel edge has to be measured with a metric that has no diagonals shorter than the grid.
One enum rather than a mode plus a corner rule. The inspector shows two dropdowns because that is how the choice reads to a person — first what kind of art, then how corners behave — but underneath there is a single value. Two fields would let them disagree (what are "4-way corners" on a smooth field?) and would have to be validated against each other everywhere they travel: into the file name, out of it, into the shader.
Values
SmoothvalueEuclidean distance with a sub-texel edge, read through bilinear filtering. Smooth at any zoom, round at corners. The default, and what every field built before 1.2.0 holds.
PixelArt8valueChebyshev distance — max(|dx|, |dy|), so a diagonal step costs the same as a straight one and the shape grows as a square. This is dilation by all eight neighbours: the outline follows diagonals and corners come out square. Written as C.
PixelArt4valueManhattan distance — |dx| + |dy|, so a diagonal costs two and the shape grows as a diamond. This is dilation by the four edge neighbours: no outline across a diagonal join, and outer corners cut back by one texel. Thinner, and what small sprites usually want. Written as M.
enum KeylineSdfSubject
FourSsets.Keyline · Runtime/KeylineSdfNaming.cs
Which component a distance field was built for, recorded in its file name as 2D or 3D.
More about KeylineSdfSubject
Why it is only a label. The pixels are identical either way — a distance field is a distance field, and a sprite's alpha and a cutout mesh's alpha are read the same. What differs is who asked for it, which is worth knowing at a glance in a project window full of them, and worth knowing when deciding which component to go and rebuild after the artwork changes.
Why the lookup honours it strictly. The two components search the same folder for a field beside the same texture, and before the mark existed they found each other's: whichever was built last, or widest, won, so a sprite silently adopted a mesh's field and a mesh rebuilt over a sprite's. The two then took turns overwriting the arrangement, and neither inspector had anything to say about it.
So a sprite looks only for 2D and a mesh only for 3D. A texture worn by both keeps two files whose pixels may well be identical — which is the price of each component owning its own range and its own threshold, and being able to change them without reaching into something else's setup.
Public because the generator is, and a public method cannot take a parameter the caller is not allowed to name.
Values
SpritevalueBuilt for a KeylineOutline2D. Written as 2D.
MeshvalueBuilt for a cutout mesh under KeylineOutlineController. Written as 3D.
enum KeylineShaderFeatures
FourSsets.Keyline · Runtime/KeylineStyle.cs
Optional Full-shader feature mask. Disabled bits hide UI / clamp selection and keep unused style / Cutout keywords off on the runtime material. Solid is always available. Player builds (WebGL) include Full-shader variants via multi_compile_local.
Values
NonevalueNo optional features.
CutoutvalueOutlines that follow the source material's alpha cutout (KeylineStyleProfile.useCutout).
PlanarExpandvalueThe sheet variant. Kept for profiles saved with it; since 1.3.0 every hull is built with sheets, and the project's Sheets feature decides whether the variant ships.
StyleNeonvalueVariants for the KeylineStyle.Neon style.
StyleHalovalueVariants for the KeylineStyle.Halo style.
StyleSketchvalueVariants for the KeylineStyle.Sketch style.
StylePatternvalueVariants for the KeylineStyle.Pattern style.
StyleDoublevalueVariants for the KeylineStyle.Double style.
StyleElectricvalueVariants for the KeylineStyle.Electric style.
StyleRainbowvalueVariants for the KeylineStyle.Rainbow style.
StyleGradientvalueVariants for the KeylineStyle.Gradient style.
StyleFrostvalueVariants for the KeylineStyle.Frost style.
StyleLavavalueVariants for the KeylineStyle.Lava style.
StyleSmokevalueVariants for the KeylineStyle.Smoke style.
StyleCrystalvalueVariants for the KeylineStyle.Crystal style.
StyleWatervalueVariants for the KeylineStyle.Water style.
StyleFirevalueVariants for the KeylineStyle.Fire style.
AllStylesvalueEvery style bit. Maintained automatically from the asset's Style field.
AllvalueEvery feature: cutout, the sheet variant and all styles.
enum KeylineShaderProfile
FourSsets.Keyline · Runtime/KeylineStyle.cs
Full feature outline shader vs the cheap Solid one.
Values
FullvalueFull Outline shader (styles, cutout, etc.).
MobileSolidvalueSolid only — best for mobile / many outlines.
enum KeylineStyle
FourSsets.Keyline · Runtime/KeylineStyle.cs
Visual style applied by the inverted-hull outline shader.
Values
SolidvalueEven contour of constant width in one hull; the cheapest style.
NeonvalueAdditive core that reads as light; Glow pushes the colour past white.
HalovalueSoft falloff built from several concentric hulls, each wider and fainter than the last.
SketchvalueHand-drawn look: width perturbed by noise and redrawn at a discrete rate.
PatternvalueTexture across the outline, such as dashes, dots or stripes.
DoublevalueTwo concentric rings with an empty gap between them.
ElectricvalueSketch's noise driven harder and faster, with a minimum width so the bolt never vanishes.
RainbowvalueHue sweep that travels along or rotates around the outline.
GradientvalueTwo-colour ramp in Linear, Radial or Planar shape; the style the wave and reveal effects drive.
FrostvalueSoft, cold edge: a wide low-opacity shell around a core, with sparkle noise.
LavavalueMolten rock: a dark crust cracked open on a hot, moving core. Frost's opposite.
SmokevalueBillowing cloud that thins and drifts. The rim dissipating rather than drawn.
CrystalvalueTorn into slices that slide and split into opposing colour fringes.
WatervalueA moving surface: crossing ripples, caustic threads, a sheen on the crests.
FirevalueTongues rising and licking away, hottest where they start.
enum KeylineStyleSpace
FourSsets.Keyline · Runtime/KeylineStyle.cs
Coordinate space a style's pattern, noise or gradient is evaluated in. Only styles with a spatial term react to this: Sketch, Pattern, Electric, Gradient, Frost, Rainbow, and the five noise styles — Lava, Smoke, Crystal, Water and Fire. Solid, Neon, Halo and Double are driven purely by rim factor and width, so there is nothing to place in space.
Values
ObjectSpacevalueSticks to the mesh — rotates and moves with the object.
WorldSpacevalueAnchored in the world — the object travels through the pattern.
ScreenSpacevalueAnchored to the screen — the pattern stays put as the camera moves.
UVSpacevalueFollows the source mesh UV0 unwrap. Appended last so existing serialized values keep their meaning. Needs a sane unwrap — meshes with overlapping or missing UVs will look wrong here, which no other space suffers from.
enum KeylineWidthMode
FourSsets.Keyline · Runtime/KeylineStyle.cs
How outline width is interpreted.
Values
WorldvalueMetres. The outline is part of the scene and shrinks with distance.
ConstantScreenvalueScreen pixels. The same thickness at any distance.
PixelsvalueTexels of the sprite's own texture, as a whole number. For 2D pixel art.
More
Why neither of the other two will do. Metres depend on 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 depend on 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.
A texel count depends on nothing. It is the unit the artist is already thinking in, it survives every zoom and every import setting, and it is the only one of the three that a grid-aligned edge can actually honour.
Not a third expansion rule. The geometry still grows by a length in metres — this is a way of writing that length down, converted through the sprite's own scale when the material is written. The shader is told World, because that is what it does.
enum SoftEdgeBodyPunch
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
How much of the object's own body is cut out of its blurred rim.
More about SoftEdgeBodyPunch
Three states, because the rim is fenced out of the silhouette but its blur is not. A rim never draws over its own body — stencil sees to that. The blur that follows has no stencil and washes inward across the silhouette, so what the object looks like from the inside is decided entirely here.
The two families reach the same look from opposite directions, which is why one label — Glow Outside Only — covers both. A Main rim exists only where its object is visible, so Visible already confines its glow and turning the switch off means dropping to None. An X-Ray rim exists where its object is hidden, which Visible deliberately spares, so confining it takes Whole.
Values
VisiblevalueCut where the body wins the depth test. Keyline's long-standing behaviour and the serialized zero, so anything authored before this became a choice keeps it.
NonevalueCut nothing — the glow washes over the object and any Interior Fill on it.
WholevalueCut the whole silhouette, hidden fragments included.
enum SoftEdgeComposite
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
How a group's isolation buffer has to be put back onto the frame.
More about SoftEdgeComposite
The buffer is not the frame, and additive content is where that stops being a detail. An alpha-blended rim drawn into a buffer cleared to transparent black lands premultiplied, and premultiplied-over puts it back correctly. An additive rim does not: its blend is One One, so the alpha channel sums along with the colour. Halo stacks up to eight concentric hulls, so that sum runs well past 1 — and the ordinary composite, One OneMinusSrcAlpha, then multiplies the frame by (1 - a) and erases everything behind the glow. The reported symptom was an X-Ray Halo that came out overexposed and the wrong colour; the scene behind it had simply been wiped.
Composited additively instead, the accumulated alpha is ignored and the colour is added exactly as it would have been on the camera. Single-layer additive styles had the same fault all along, too mildly to notice.
Only these two, and the rest are a documented limitation. Screen, Multiply, Darken, Lighten and Subtract all read the destination, and in a private buffer the destination is zero rather than the scene — Multiply and Darken collapse to black before any composite could rescue them. They cannot be reproduced through an isolation buffer at all, and no choice of operator here changes that.
enum SoftEdgeHaloDepth
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
Which way a halo's depth test points, or that it has none.
More about SoftEdgeHaloDepth
Direction, not just on/off, because the two rim families are mirror images. A Main rim draws where its object is in front of the scene; an X-Ray rim draws where it is behind. Their blurs smear across those boundaries in opposite directions — Main onto the wall that hides it, X-Ray off the wall it is seen through and onto open space. One comparison serves both; only its sense follows the family.
It is an enum rather than two booleans so it can sit in the softness group key. A group shares one composite, and the composite is where the test happens: Main and X-Ray carrying the same Edge Softness would otherwise land together and one of them would get the other's direction.
Values
IgnorevalueNo test. The halo draws wherever the blur reached.
WhereVisiblevalueKeep the halo only where its emitter is in front of the scene — the Main rule.
WhereHiddenvalueKeep the halo only where its emitter is behind the scene — the X-Ray rule.
enum SoftEdgeIsolationPolicy
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
Which X-Ray draws leave the camera when the Soft Edge pass is installed. Set by the live URP feature or HDRP custom pass — not inferred from package defines.
Values
RimsOnlyvalueURP: isolate X-Ray rims only. Interior Fill stays on the camera.
FullXRayFamilyvalueHDRP: isolate X-Ray Outline and Interior. The camera stencil has only two user bits, so Fill composites first in the private RT.
Advanced and pipeline
Render-pipeline plumbing and low-level configuration. Public so the pipelines can reach it; game code rarely needs it.
interface IKeylineMergeable
FourSsets.Keyline · Runtime/IKeylineMergeable.cs
What automatic merging needs to know about an outline, whichever component draws it.
More about IKeylineMergeable
Why this exists. Merging by overlap was written against the mesh controller and named it in every collection, so a sprite outline could not join a cluster however much it overlapped one. The two halves of the product had ended up with different halves of the same feature: the mesh side merged by proximity and could not merge with its children, the sprite side merged with its children and could not merge by proximity.
The surface turned out to be three members, and the sprite component already had two of them — which is the usual sign that the type name in the collections was the only thing standing in the way.
Properties
Component AsComponent { get; }propertyThe behaviour itself.
More
Not decoration. Unity's overloaded == reports a destroyed object as null; a reference held as an interface does not go through that overload and answers "not null" for an object that is gone. Everything that checks liveness has to check the component, and everything that walks the scene needs it too.
bool IsOutlineEnabled { get; }propertyWhether this outline is currently drawing anything worth merging.
Whether this outline is willing to be pulled into a cluster by overlap at all.
More
Asked here rather than at registration so that turning it off takes effect on the next tick instead of on the next enable.
bool MergeChildOutlines { get; }propertyWhether this outline takes the outlines beneath it into its own figure.
More
Read by the separation test, not by clustering itself: a parent that refuses its children is saying they are not one silhouette, and overlap must not put them back together behind its back.
How far this outline's widest active rim reaches past those bounds, in metres.
More
Asked separately rather than folded into the bounds. Those bounds are read by the draw-order sorter and by the recipes as well, and every one of them wants the object, not the object plus its rim. Merging is the only rule that needs the wider figure, so it is the only one that asks for it.
See KeylineStyleProfile.HullWorldReachMeters for what the number covers — and for the one case it answers 0 on purpose.
Methods
The outline's own bounds in world space, or false when there is nothing drawn.
Called when the stencil this outline should use has changed underneath it.
class KeylineBakedMeshEntry
FourSsets.Keyline · Runtime/KeylineOutlineController.cs
Persisted outline mesh from Bake for Runtime.
Fields
The mesh the outline was baked from.
The saved outline mesh used in place of sourceMesh.
The outline settings the mesh was baked for; a different value means it is stale.
class KeylineCustomDrawConfig : ScriptableObject
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Queue, ZTest, ZWrite, Cull, depth bias and stencil for every 3D Keyline draw. Factory values are the current working defaults (in-memory when no scene host). Optional: assign from a scene KeylineDrawConfig component.
Properties
public static KeylineCustomDrawConfig Factory { get; }propertystaticIn-memory factory. Used when the scene has no KeylineDrawConfig host (or its Custom Draw Config slot is empty).
Methods
Raises Changed for this asset.
Returns a copy of the settings for a stage, or default for an unknown id.
Replaces the settings for a stage. Does not raise Changed; call RaiseChanged afterwards.
Returns the queue offset of the family a pass belongs to.
More
| Parameter | |
|---|---|
occluded | True for the X-Ray passes. |
overlay | True for a cutout overlay pass. |
Returns the queue offset of layer within its family.
Converts a mask setting to a stencil bit mask; custom is used, limited to 8 bits, for KeylineDrawStencilMask.Custom.
Returns the stencil reference for mode from the outline's claim value and the hollow bit.
Converts a cull setting to a CullMode value; KeylineDrawCull.UseProfile returns profileCull.
Resets every setting of config, including all stages, to the factory values.
Resets the base queue, Prefer Material Queue and the family and layer offsets to the factory values.
Resets Auto-Sort Overlapping X-Ray and the slot stride to the factory values.
Resets every stage to the factory values, leaving queues and biases alone.
Makes sure there is one stage per KeylineDrawStageId, filling missing ones with factory values and keeping existing ones.
Events
Raised by RaiseChanged when an asset's values change, so outlines can reapply them.
Fields
public const int StageCount = 17fieldstaticNumber of draw stages, one per KeylineDrawStageId.
Factory baseQueue: the Transparent queue plus 50.
public const int FactoryFamilyMain = 0fieldstaticFactory familyMain.
public const int FactoryFamilyOverlay = 16fieldstaticFactory familyOverlay.
public const int FactoryFamilyXRay = 32fieldstaticFactory familyXRay.
Factory familyXRayOverlay.
Factory layerSelfOcclude.
public const int FactoryLayerFill = -3fieldstaticFactory layerFill.
public const int FactoryLayerClipClaim = -2fieldstaticFactory layerClipClaim.
layerClipClaim value that puts the clip claim in the mask's queue, as before 1.3.0. What a Draw Config saved before the field existed reads.
public const int FactoryLayerMask = -1fieldstaticFactory layerMask.
public const int FactoryLayerRim = 0fieldstaticFactory layerRim.
public const int FactoryLayerGap = 1fieldstaticFactory layerGap.
public const int FactoryLayerExtra = 2fieldstaticFactory layerExtra.
public const int FactoryXRaySlotStride = 32fieldstaticFactory xRaySlotStride.
Factory cutoutClipBias.
Factory coplanarClipBias.
Factory companionWorldBiasAuto: -1, the automatic bias of about one part in ten thousand of view depth.
public static readonly string[] StageTitlesfieldstaticInspector titles of the stages, indexed by KeylineDrawStageId.
public int baseQueuefieldUsed when Prefer Material Queue is off, or the outline material has no queue.
When on, the outline material's render queue is the base (historic Keyline).
Queue offset added to the base queue for the Main passes.
Queue offset added to the base queue for the Main cutout overlay pass.
Queue offset added to the base queue for the X-Ray passes.
Queue offset added to the base queue for the X-Ray cutout overlay pass.
Queue offset within a family for the self-occlude stamp.
public int layerFillfieldQueue offset within a family for the interior fill.
public int layerMaskfieldQueue offset within a family for the hollow mask.
Queue offset within a family for Clip At Intersections' claim, or ClipClaimWithMask to share the mask's queue.
More
Why the claim has a queue of its own. The claim and the mask are both stamps of the body, with different stencil and depth state, and they have to come after the fill and before the rim. In one queue they sort by distance with every other outline's, so they alternate object by object — mask, claim, mask, claim — and every switch is a SetPass call: about two per object, which kept outlines with Clip At Intersections on out of the SRP Batcher. In queues of their own each is one batch. Which of the two draws first does not matter: the claim writes only where no mask has claimed, and the mask overwrites it where it has.
A Draw Config saved before 1.3.0 has no such field and reads ClipClaimWithMask, so its queues stay exactly as they were; Reset Queues moves it to the factory layout.
public int layerRimfieldQueue offset within a family for the rim.
public int layerGapfieldQueue offset within a family for Double's gap mask; Halo's layers step up from it.
Queue offset within a family for extra hulls: Frost's shell and Double's outer ring.
Nearer X-Ray families finish before farther ones stamp the same camera stencil.
Queue span reserved for one object's X-Ray family.
Clip bias that Auto Clip Bias gives Main draws of an alpha-cutout outline.
Clip bias that Auto Clip Bias gives Main draws of a hull with sheets, or with Face Cull Both.
World-space bias that Auto Clip Bias gives X-Ray draws, in metres; -1 picks one from view depth.
struct KeylineDrawStage
FourSsets.Keyline · Runtime/KeylineCustomDrawConfig.cs
Depth, cull, depth bias and stencil state for one KeylineDrawStageId.
Fields
Depth test for this draw.
public bool zWritefieldWrite the depth buffer.
UseProfile reads Face Cull on the outline profile.
When on, clip bias uses Cutout / Coplanar constants from the Queues tab.
Pull toward the camera as a fraction of view depth, used when Auto Clip Bias is off and depthBiasWorld is 0. Shown as Clip Bias.
World-space bias.
Stencil comparison against the reference value.
Stencil operation when the stencil test passes.
Stencil read mask.
Stencil write mask.
Read mask used when readMask is KeylineDrawStencilMask.Custom (0-255).
Write mask used when writeMask is KeylineDrawStencilMask.Custom (0-255).
Which stencil reference value the stage uses.
ZTest follows the host outline (Gap / Extra Hull on X-Ray stay Greater).
Stencil bits follow the host outline.
class KeylineHdrpPassFlags
FourSsets.Keyline · Runtime/KeylineHdrpPassFlags.cs
Whether each HDRP pass is switched on, as the runtime sees it.
More about KeylineHdrpPassFlags
Three properties and no storage of its own, deliberately. Who fills them depends on where the code is running, and both answers arrive before anything reads them:
- In the editor, the settings page pushes them on load and on every change — the same arrangement
KeylineEditorBehaviouruses to hand editor-only preferences to runtime code that must not reference UnityEditor. - In a player,
KeylineRuntimeManifestreads them out of Resources, where the build preprocessor left them.
Separate from KeylineRuntimeFlags because these three answer for one pipeline and mean nothing on the others, while those apply everywhere. They share a courier, not a subject.
Properties
public static bool XRayLayerFiltering { get; set; }propertystaticWhether the X-Ray occluder custom pass should be running.
public static bool SoftEdge { get; set; }propertystaticWhether the Soft Edge custom pass should be running.
Layers whose transparent geometry counts as an X-Ray occluder. Zero — no layer — is the default and means the pass collects the opaque queue and nothing else, as it always has.
class KeylineHdrpVolumes
FourSsets.Keyline · Runtime/Pipelines/HDRP/KeylineHdrpVolumes.cs
Finding and removing Keyline's own Custom Pass volumes — the one piece both HDRP bootstraps need and neither should own.
More about KeylineHdrpVolumes
The whole reason this file exists is the search. Keyline spawns its pass volumes hidden and unsaved, which is what keeps them out of the buyer's hierarchy, out of their saved scenes and out of version control. It is also what makes them invisible to FindObjectsByType, which skips anything flagged DontSave — so the first version of this searched, found nothing, and cheerfully created a second volume next to the one already running. Two volumes means the prepass runs twice, and removing the feature deleted only the one the search could see: the other kept re-arming it every frame, and the switch looked broken.
Resources.FindObjectsOfTypeAll is the search that does see them. It also sees objects inside prefab assets, which are not in any scene and must not be touched, so every result is checked for a valid scene before it counts.
Methods
public static CustomPassVolume Find(string name)methodstaticThe live volume with this name, hidden or saved, or null.
Every live volume with this name — there should never be more than one.
public static int CountSaved(string name)methodstaticHow many volumes with this name are saved into a scene rather than spawned hidden.
More
Keyline no longer makes these; a project upgraded from a version that did still has them, and they still work, which is exactly why they need pointing out. Left alone they look like something Keyline requires, and the buyer copies one into every new scene.
Leave exactly one volume with this name alive, and say which.
More
The case this closes. A hidden volume is spawned while the open scene has none. The buyer then opens a scene that a Keyline 1.1 project saved one into — and now two are running. For the X-Ray prepass that is only wasted work, because BlendOp Min writing the same depths twice lands on the same answer. For Soft Edge it is visible: the halo is composited twice and the rim comes out brighter and wider than the profile says.
A saved volume wins. It is the buyer's GameObject, in their scene and their version control; deleting it here — on a timer, with no prompt — is not something a package gets to decide. Only volumes Keyline spawned hidden are removed, which is why the flag is checked rather than assumed. The settings page is where a saved one is offered for deletion, with a button and an explanation.
Destroy every volume with this name, plus one named outright. Returns how many.
More
The second argument is the important one. A search can come back empty for reasons that are not "there is nothing there" — and when it does, a caller that trusted it clears its own reference and the volume becomes both alive and unreachable. That is an off switch that can never work again, and it is worth one parameter to make impossible. Callers pass the volume they are holding; it is destroyed whether or not the search agrees it exists.
public static int DestroyHidden(string name)methodstaticDestroy every hidden volume with this name — the ones Keyline spawned — and leave anything saved in a scene where it is. Returns how many went.
More
The safety net behind the off switch. Off normally works by reference and never searches; this is what runs on the slow sweep instead, so that a hidden volume which somehow outlived the reference to it cannot keep rendering for the rest of the session. There is no way to reach such an object by hand — it is not in the hierarchy — so if nothing here looked for it, nothing ever would.
public static void Destroy(GameObject go)methodstaticThe right destroy for the mode we are in.
More
Object.Destroy defers to the end of the frame and does nothing at all outside play mode, so a volume removed from the settings page would still be rendering when the page redrew and reported it gone. DestroyImmediate in play mode is the opposite mistake — Unity warns about it, and it can run while the object is mid-render.
class KeylineLodCullProxy : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineLodCullProxy.cs
Marks the hidden LOD Group an outline adds beside a user's LOD Group, so the outline disappears where the group culls the object. Created and destroyed by KeylineOutlineController; never saved, never added by hand.
More about KeylineLodCullProxy
Why a second group rather than the user's own. Adding the outline's renderers to the user's LOD Group would have the scene save references to objects that are never saved, and the group would load with holes in it. This group has one level, holds only the outline's renderers, and copies the user group's size, reference point and cull height, so Unity culls it at the same moment as the object.
Properties
The outline that made this proxy.
public LODGroup Source { get; }propertyThe user's LOD Group this proxy follows.
public LODGroup Group { get; }propertyThis proxy's own one-level LOD Group.
class KeylineMergeResolver
FourSsets.Keyline · Runtime/KeylineMergeResolver.cs
Merges outline silhouettes when primary controllers' world bounds overlap. Explicit KeylineMergeGroup still force-merges non-overlapping members and excludes those controllers from auto clusters.
Methods
public static void Invalidate()methodstaticRe-cluster on the next tick.
More
For the switches that live above overlap in the ranking — Merge With Children, a group's Group Children. Clustering cannot see them change: nothing about a registered outline differs, only the answer the separation test now gives about a pair of them. Without this the toggle appeared not to work for up to a tenth of a second, which in the Editor reads as it not working at all.
Adds an outline to the set considered for merging and schedules an immediate re-cluster. Creates the hidden driver object on first use. Ignores null and repeated calls.
Removes an outline from merging, drops its cluster assignment and schedules an immediate re-cluster.
public static void Tick(bool force = false)methodstaticRebuild overlap clusters. Rate-limited: this walks every registered controller's renderer hierarchy, which is far too expensive to do every frame.
class KeylineMirrorFolder : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineMirrorFolder.cs
The one child a Keyline outline adds to an object it draws, and the parent of every mirror built for that object.
More about KeylineMirrorFolder
Why it exists. A pass builds up to eight mirrors per source renderer — the rim, the interior mask, the two intersection-clip draws, a shell or a ring pair or one per halo layer — and a component has up to six passes. An object made of a dozen meshes therefore sprouted a few hundred loose children, all named __KLOutline_*, all sitting directly under the renderers they mirror. The Hierarchy window became unreadable, which is a real cost: it is where a person looks when something is wrong.
One folder per source renderer, shared by every pass, so the hierarchy shows one collapsed entry where it used to show the lot.
It is also what makes "is this renderer ours?" cheap. The source walk used to ask every renderer under an outlined object for a KeylineMirrorTag, and after a build most of the renderers under an outlined object are mirrors — so the walk grew with the thing it was trying to skip. A mirror now lives under a folder, and a folder is one reference comparison away.
Created with HideFlags.DontSave like the mirrors it holds, so it never reaches a saved scene and a project opened in 1.2.0 sees nothing of it.
Properties
Mesh outline that owns this folder, or null when a sprite one does.
Sprite outline that owns this folder, or null when a mesh one does.
public bool IsAbandoned { get; }propertyTrue when the component that made this folder is gone and it is garbage.
Methods
public static bool Holds(Renderer renderer)methodstaticTrue when this renderer is a Keyline mirror, answered without a GetComponent.
Take the folder out of Scene view picking, or put it back.
More
Applied to the folder as well as to the mirrors under it. The folder has no renderer of its own, but Unity's picking walks a hierarchy, and a parent whose children are all unpickable should not itself be the thing a marquee grabs.
Fields
public const string ObjectName = "__Keyline"fieldstaticWhat the folder is called in the Hierarchy window.
class KeylineMirrorTag : MonoBehaviour
FourSsets.Keyline · Runtime/KeylineMirrorTag.cs
Marks a generated outline mirror object and records which controller / pass owns it. Replaces the previous name-based detection ("__KL_Outline" and friends), which broke in two ways: a user object legitimately named "__Something" was silently skipped as an outline source, and Unity's " (1)" duplicate suffix had to be parsed back off by hand.
Properties
Controller that created this mirror (null once the controller is gone).
Sprite outline that created this mirror, or null when a mesh did.
Pass this mirror belongs to.
public bool IsAbandoned { get; }propertyTrue when the owner no longer exists — the mirror is garbage.
Methods
public void Bind(KeylineOutlineController owner, KeylinePassKind pass, bool cutoutOverlay = false)methodRecords the mesh controller and pass that own this mirror, clearing any sprite owner.
More
| Parameter | |
|---|---|
cutoutOverlay | True when the mirror draws the pass's cutout overlay rather than the pass itself. |
The sprite half.
More
Added because the sprite component was tagging nothing at all, and the tag is how everything else in the package — and everything a buyer writes — tells Keyline's own renderers apart from the object's. The visible cost was the Reveal recipe: it hides the body and leaves the outline, and on a sprite it collected the outline as part of the body and hid that too, so the effect became the object blinking off and on.
True when this mirror belongs to the given controller, pass and overlay draw.
Take this mirror out of Scene view picking.
More
The problem it solves. A mirror sits exactly on top of the object it outlines and is usually a little larger, so a click in the Scene view lands on the mirror rather than on the thing anybody meant to select. Drag it and the outline is now somewhere the object is not — and the next rebuild throws the mirror away, so the fix looks like the tool losing the edit. It is the kind of papercut that reads as the asset being broken.
There is no attribute for this. [SelectionBase] has no opposite; it redirects a click up the hierarchy but cannot refuse one. What does refuse it is SceneVisibilityManager's picking state — the hand icon in the Hierarchy window — which is exactly the mechanism Unity offers for "visible, not clickable".
Here rather than at the two places mirrors are built, because every mirror carries this tag by definition: that is what the tag is for. A third kind of mirror added later gets the behaviour without anybody remembering to ask for it.
Nothing is restored when the mirror dies. The state belongs to a GameObject that is created with HideFlags.DontSave and destroyed on the next rebuild, so there is no scene data to leave behind and nothing for a buyer to clean up.
Both directions, because the switch has two. This used to disable picking and nothing else, so turning the setting off left every mirror already in the scene unpickable until something rebuilt it — the setting looked one-way and read as broken. It now says what the setting says, which also means the answer can be re-applied to mirrors that already exist rather than only to the next ones built.
Called by whoever built the mirror, once it is finished — not from OnEnable here. A component's OnEnable runs the instant it is added, and at that moment the mesh path has not parented the object yet and neither path has given it a renderer. The visibility manager is being told about a renderer-less object sitting at the root of the scene, and the answer it files there does not follow the object to where it ends up. That is why the first attempt at this changed nothing.
Marked Conditional so the call sites need no compilation fences of their own, and so the whole thing disappears from a player build.
class KeylineRuntimeManifest : ScriptableObject
FourSsets.Keyline · Runtime/KeylineRuntimeManifest.cs
The project-wide settings a player build has to read, in the one form it can read them.
More about KeylineRuntimeManifest
Why this type has to exist at all. Keyline's project settings live in ProjectSettings/KeylineSettings.asset — outside Assets, so reimporting the package cannot overwrite them, and in version control, so a team shares one answer. A player build cannot read that folder. Resources is the one place a player will look. So the answer is not to move the setting but to copy it: written just before the build, read by the player at startup, deleted immediately afterwards.
Not every setting rides. Only the ones that change what the runtime does. The editor conveniences — mirror picking, rebuild on hierarchy change — have no meaning in a player and are not carried.
It is generated, used and deleted by the build. Nothing creates it during ordinary work and nothing should be checked into version control. A build that changes none of the carried settings writes no manifest at all, and the defaults on KeylineRuntimeFlags and KeylineHdrpPassFlags are the shipped behaviour, so the absence is a complete answer.
The bug it exists to prevent. The HDRP pass switches used to live in PlayerPrefs, which is per-machine and per-application: the editor's prefs and a player's prefs are different stores, so a fresh build read nothing, spawned no volume, and shipped with the pass off — on a machine other than the developer's, a build server for instance, the editor did not agree with itself either.
Properties
Whether the X-Ray occluder pass should run. HDRP only.
Whether the Soft Edge pass should run. HDRP only.
Layers whose transparent geometry the X-Ray occluder pass should collect as well.
More
Travels with the two switches because it has the same problem: a project-wide value the player has to read, kept somewhere the player cannot look.
The project's feature set, as ticked — see KeylineFeatures.
More
Carried only when it is not Everything. A manifest that does not carry it leaves every feature on, which is the shipped behaviour, so an older manifest and a missing one both mean the same thing.
Whether this manifest says anything about features at all.
Fields
The manifest's file name in Resources, without extension.
class KeylineSdfFieldModes
FourSsets.Keyline · Runtime/KeylineSdfNaming.cs
Conveniences for reading a KeylineSdfFieldMode.
Methods
True for anything that must be drawn on the texel grid.
class KeylineSoftEdgeCustomPass : CustomPass
FourSsets.Keyline · Runtime/Pipelines/HDRP/KeylineSoftEdgeCustomPass.cs
Optional HDRP pass: same job as KeylineSoftEdgeFeature in URP. Hosted on a global CustomPassVolume created by Add Soft Edge Pass.
Fields
Name of the GameObject holding the global Custom Pass volume that runs this pass; used to find existing volumes.
class KeylineSoftEdgeDraw
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeDraw.cs
Draw registered rim mirrors into an RT, Kawase-blur in world units, composite. Shared by the URP feature and the HDRP custom pass so the two pipelines cannot drift.
Properties
public static Material Material { get; }propertystaticShared soft-edge material, created on first use; null when the shader cannot be found. Rebuilt if the cached one is missing passes, for example while the shader imports.
Methods
Records every registered soft-edge renderer into cmd in registry order, skipping body-only entries.
Copy camera depth once, then isolate each owner so two X-Ray objects cannot overwrite each other's stencil claim on the same overlap pixels. Claims (mask / self-occlude) stamp first, Fill composites next (no Punch), then rim softness groups; stencil is kept so Main fences X-Ray and merge-group Outline unions like Fill.
More
| Parameter | |
|---|---|
haloDepth | Screen-sized two-channel target holding, per pixel, how near the thing that emitted the halo there is and how far the dilation carried it — the second is what lets the composite judge a pixel by the emitter beside it rather than by whichever emitter is nearest the camera. Only read for groups that asked for occlusion; pass anything when |
haloDepthTmp | Ping-pong partner for the dilation of the above. |
haloDepthValid | Whether the two targets above exist. Both pipelines allocate them only when some live rim is set to |
Records renderer with its shared materials into cmd, even when the renderer is hidden from the camera with forceRenderingOff. Does nothing for a disabled renderer.
Copy camera depth into a private depth target so Soft Edge stencil stays off the camera's stencil (hard interiors keep their own fence). Must keep colour+depth bound: CommandBuffer.Blit retargets colour-only and the hull then ZTests against an empty buffer — Main Outline draws through occluders.
Ping-pong Kawase on src via tmp, punch this owner's source silhouette out of the blur, then composite onto dest (owned RGBA accumulation, not camera color). Radius is screen pixels already converted from world centimetres at the object's depth, so the halo stays around the mesh instead of spreading as the camera pulls away. Offset scales with the fractional radius so 1.3 px is not 1.7.
Premul-blend src onto dest. dest must already be the bound target (raster pass) or will be bound with Load (command buffer).
Fields
Name of the soft-edge shader, used when the Resources material is missing.
Resources path of the soft-edge material template, loaded before falling back to ShaderName.
public const int PassComposite = 1fieldstaticShader pass index that composites the soft-edge result, read from SrcColorId, over the camera colour.
public static readonly int SrcColorIdfieldstaticGlobal texture id _KeylineSoftEdgeSrcColor: the source colour the composite pass reads.
public static Material PunchOverridefieldstaticHDRP Custom Pass swaps this for a punch shader that uses TransformWorldToHClip. Null keeps the shared UnityCG punch (URP).
public static int PunchOverridePassfieldstaticPass index on PunchOverride, or the shared punch pass.
public static int PunchOverrideWholePassfieldstaticPass index on PunchOverride for the whole-silhouette punch — the same draw with ZTest Always, used by X-Ray Interior Over Rim.
public static Material HaloSeedOverridefieldstaticSame swap, for the pass that seeds the halo depth field from the owner's bodies.
More
Two overrides rather than one because only geometry passes need a pipeline of their own: the vertex stage has to reach a view-projection matrix, and HDRP publishes that from a different header than UnityCG. Everything else the halo field needs — the dilate and the composite — is a full-screen blit, and those already run under both pipelines from the shared shader.
public static int HaloSeedOverridePassfieldstaticPass index on HaloSeedOverride, or the shared seed pass.
public static Material SceneFieldOverridefieldstaticThe pass that turns the camera's depth into the normalised field the composite compares against. Overridden on HDRP for the same reason the seed is: reading HDRP's camera depth takes CustomPassLoadCameraDepth, which UnityCG has no equivalent for.
public static int SceneFieldOverridePassfieldstaticPass index on SceneFieldOverride, or the shared one.
class KeylineSoftEdgeFeature : ScriptableRendererFeature
FourSsets.Keyline · Runtime/Pipelines/URP/KeylineSoftEdgeFeature.cs
Optional URP pass: draws Main/X-Ray rims into an RGBA RT (camera depth for ZTest, private stencil so hard interiors keep theirs — and so two X-Ray objects cannot overwrite each other's claim). Kawase-blurs a world-space halo when Edge Softness > 0. Composites after transparents. PunchBody then cuts the source silhouette out of the blur so the glow sits around the object, not on its albedo. Add via Tools on Keyline Outline 3D.
Methods
Creates the render pass and reports the Soft Edge pass as installed to KeylineSoftEdgeRegistry.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)methodEnqueues the pass for each camera except previews, reflection probes and single-pass stereo, requesting an intermediate texture only while KeylineSoftEdgeRegistry.HasAny is true.
class KeylineSoftEdgeHdrpBootstrap
FourSsets.Keyline · Runtime/Pipelines/HDRP/KeylineSoftEdgeHdrpBootstrap.cs
Keeps a hidden global Custom Pass volume alive whenever the buyer has switched Soft Edge on, so no scene ever has to hold one.
More about KeylineSoftEdgeHdrpBootstrap
The twin of KeylineXRayOccluderHdrpBootstrap, which carries the long form of every comment shared by the two. Read that one first; this file differs only in which pass it hosts, where it injects, and the registry it arms.
Properties
public static bool HasHiddenVolume { get; }propertystaticWhether a hidden volume of ours is currently alive.
public static bool HasVolume { get; }propertystaticWhether any volume is alive to run the pass.
public static int SavedVolumeCount { get; }propertystaticVolumes a pre-1.2 Keyline saved into the buyer's scenes, which are now surplus.
Methods
public static CustomPassVolume EnsureVolume()methodstaticA volume exists and is running the pass. Safe and cheap to call repeatedly.
public static void Deduplicate()methodstaticMake sure only one volume is running, and hold on to the survivor. It matters more here than for X-Ray: two Soft Edge passes composite the halo twice, and the rim comes out brighter and wider than the profile says.
Take the hidden volume away, leaving anything the buyer made themselves alone. See KeylineXRayOccluderHdrpBootstrap.DestroyHiddenVolume for what thorough buys.
public static int DestroyVolumes()methodstaticTake every Keyline Soft Edge volume away — only from an explicit Off. Returns how many. The held volume is passed in rather than left to the search; see KeylineXRayOccluderHdrpBootstrap.DestroyVolumes for what happens when it is not.
class KeylineSoftEdgeRegistry
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
Rim mirrors that the Soft Edge pass should draw (Main / X-Ray / Overlay). Isolation follows IsolationPolicy: URP keeps Interior Fill on the camera; HDRP takes the whole X-Ray family off so each object's stencil lives in a private 8-bit RT. Fill entries are tagged Entry.IsFill so they composite before Mask/Outline. Mask and self-occlude are tagged Entry.IsClaim so they stamp the shared body claim before any rim colour — merge-group X-Ray Outline then unions like Fill.
More about KeylineSoftEdgeRegistry
Stamp-only entries (hard Main, still on the camera path) are redrawn into the private RT so that object's X-Ray can fence against its own Main band without a second composite.
Body-only entries are the owner's source mesh. They stay on the camera path and are drawn after Kawase with PunchBody so the blur does not cover the object's albedo.
The pass is optional. When it is not in the pipeline, PassInstalled is false and controllers leave mirrors on the camera's transparent path — a hard edge.
Properties
public static bool PassInstalled { get; set; }propertystaticTrue while a URP feature or HDRP custom pass is actually running. Controllers read this before taking mirrors off the camera path.
More
Turning it on has to tell the controllers, and not doing so was a bug. A domain reload wipes this static, and the controllers' OnEnable runs before the pass has had a chance to announce itself. So every controller re-registered while the answer was still false, left its mirrors on the camera, and nothing ever asked again — the soft edge vanished on every recompile and came back only when somebody touched Edge Softness, because that forced the refresh by hand.
The setter is also where the per-frame writes are absorbed. Both pipelines set this every time they record, deliberately — a pass that is running is the only thing that can say so — and comparing first turns that heartbeat into nothing.
Which X-Ray draws leave the camera. The live pass sets this; default is URP-safe.
More
Announced like PassInstalled, and for the same reason: it decides whether the X-Ray family is taken off the camera, so a controller that registered under the old answer is registered wrongly.
public static bool PassRanRecently { get; }propertystaticTrue while a pass has actually recorded within the last few render contexts.
More
Not the same question as PassInstalled, and conflating them was a bug that put outlines out of sight. PassInstalled answers "a pass object exists, so registering is worth doing" — URP constructs a renderer feature whether or not its tick box is on, and HDRP builds the volume before the camera's frame settings are consulted. Isolation is a different question: taking a rim off the camera is only safe if something is going to draw it again, and the only proof of that is a pass that ran.
So an unticked feature, a camera whose frame settings exclude custom passes, and URP's compatibility mode — where the render-graph recording never happens — all land in the same honest state: no soft edge, and rims drawn the ordinary way. Before this they landed on rims taken off the camera and composited by nobody.
False while the project has Soft Edge switched off, whatever the pipeline is doing: a pass still installed on a renderer must not pull rims off the camera for a feature the project has said it does not use.
public static bool PassEverRan { get; }propertystaticCalled by a pass from inside its own recording, and from nowhere else.
More
Construction does not count and must not call this: that is the whole distinction above. The first arrival flips the answer and announces it, which is what makes the controllers register — the same bootstrap PassInstalled already relies on.
Public for the same reason PassInstalled is: the pipeline halves live in their own assemblies, which do not see this one's internals.
public static bool HasAny { get; }propertystaticTrue when any entry other than a body-only one is registered.
public static bool AnyHaloOccluded { get; }propertystaticWhether any live rim wants its halo depth-tested at all, in either direction.
More
Asked by both pipelines before they allocate the depth field the test needs. Match Rim is the default, so the answer is yes as soon as anything in view carries Edge Softness above zero. A scene with no soft rim at all — which is most scenes, since Edge Softness itself starts at zero — still allocates no extra targets and runs no extra passes, and so does one whose profiles have been put back on KeylineHaloOcclusion.IgnoreDepth.
Methods
public static bool StereoArrayInUse()methodstaticWhether the camera is rendering into a stereo texture array, which this pass cannot address — and if so, says once why the soft edge is not there.
More
The shaders are single-eye. The composite reads its source with Texture2D.Load, which takes no slice index, and the render targets are built without an array dimension; under single-pass instanced or multiview the camera's buffers are arrays, and the mismatch is a read that returns nothing useful into a blend that runs over the whole frame. Standing aside is the honest answer: the outlines draw the ordinary way, because nothing is taken off the camera unless a pass reports having run.
Multi-pass is left alone. It renders one eye at a time into ordinary two dimensional targets, which is exactly what these shaders expect.
The X-Ray occluder half of the package is array-aware and unaffected; this is about the soft edge only.
public static void NotePassRan()methodstatic<remarks>Public for the pipeline assemblies, like the two properties above.</remarks>
Registers renderer as a soft rim, or updates its existing entry, replacing a stamp-only entry for the same renderer. Negative softness is treated as 0.
Source mesh of this owner. Drawn after Kawase to punch the silhouette out of the blurred rim so the glow sits around the object, not on it. Stays on the camera path.
Removes the body entry for renderer, if there is one.
public static void UnregisterBodies(int ownerId)methodstaticEvery body this owner registered. A walk, and it stays one: the question names an owner rather than a renderer, and it is asked once per owner rather than once per renderer.
Draw this renderer into the private RT for stencil only. It stays on the camera path (hard Main). Ignored when the same renderer is already a colour entry (soft Main).
public static void Unregister(Renderer renderer)methodstaticRemoves the rim or stamp entry for renderer, if there is one.
Removes the entry for renderer only while it is still stencil-only, leaving a colour rim in place.
public static List<Entry> CopySorted()methodstaticSorted by render queue so Main still draws before X-Ray inside the RT.
More
The sort is kept on Live rather than redone on a copy every frame: the set is the same from one frame to the next unless something registered or was removed, and _orderDirty is what those say. The copy is still handed out so the draw cannot be walking the list while a registration edits it.
public static void RefreshControllers()methodstaticCalls KeylineOutlineController.RefreshAppearance on every 3D outline, inactive ones included, for example after the pass is installed or removed.
struct KeylineSoftEdgeRegistry.Entry
FourSsets.Keyline · Runtime/SoftEdge/KeylineSoftEdgeRegistry.cs
One renderer the Soft Edge pass draws, with what it needs to order, group and composite it.
Fields
The renderer to draw: an outline mirror, or the owner's source mesh for a body entry.
Edge Softness of the rim; rims of one owner with the same softness share a blur. 0 for stamp and body entries.
Render queue of the renderer's material when registered; entries draw in this order.
Identifies the outline, or the merged silhouette, the entry belongs to; each owner is isolated in its own pass.
Drawn for stencil only; the renderer stays on the camera path as a hard Main rim.
The owner's source mesh, drawn after the blur to punch the silhouette out of the glow.
Interior fill, composited before mask and outline.
Mask or self-occlude stamp that writes the body claim before any rim colour.
Which way this rim's halo tests depth, from KeylineHaloOcclusion on its profile and the family it belongs to.
More
Carried per entry rather than read from the profile at draw time, because the draw side has renderers and not profiles — and because it is what the softness groups are split on. Two rims that blur together must agree about this: they share one blur and one composite, and the composite is where the test happens.
This rim is cut out of the object's whole silhouette, not only the part that wins depth.
More
On the rim, not on the body, and that distinction was a bug once. The punch draws bodies, so hanging the flag there looked natural — and made it one value per object, so Main and X-Ray could not disagree. But the punch runs inside each softness group over that group's own buffer, which is per rim family. Carried here and folded into the group key, the two families are cut independently.
How this rim's group has to be put back onto the frame. See SoftEdgeComposite — it is in the group key because a group shares one composite, and Main alpha next to X-Ray additive at the same Edge Softness would otherwise be put back with one operator for both.
How far this rim reaches outside the body it hugs, and in which units.
More
It has to come from the profile, and that is not a shortcut. The hull is extruded in the outline shader's vertex stage, so the mirror's bounds — the obvious place to measure it — know nothing about it and report the source mesh. Without this number the halo depth field is seeded from the body and stops a rim's width short of the colour it describes, and every attempt to test occlusion then lands on the field's own frontier instead of on the occluder.
ReachIsPixels because Width Mode decides the unit: World is metres and has to be projected at the object's distance, Screen is already pixels and must not be projected twice.
True when Reach is in screen pixels, false when it is in metres.
Which channels of the X-Ray occluder buffer are allowed to cut this rim's glow, in four bits, or zero for the camera's depth.
More
Main Layer Filtering lets a rim draw in front of chosen layers, and the cut at the composite has to agree with it or the glow is sliced along the very edge the rim was let through. Non-zero means the composite tests against the nearest occluder those channels hold instead — see KeylineXRayBlockerField.hlsl.
In the group key, for the same reason the composite operator is: a group shares one cut, and two rims cut against different occluders cannot share it.
Constructors
Creates an entry. Each argument sets the field of the same name.
class KeylineSorting2D
FourSsets.Keyline · Runtime/KeylineSorting2D.cs
Turns a sprite's place in the sorting order into a depth value the four passes can compare.
More about KeylineSorting2D
This is the one idea the whole of Keyline 2D rests on. In 3D the depth buffer answers "is this pixel behind something", and the four passes are built on that single question — LessEqual draws the visible part, Greater draws the hidden one. A 2D scene leaves that buffer empty, so the question has no answer and X-Ray cannot exist. Rather than invent a second mechanism for 2D, Keyline supplies the missing input, and the existing machinery works unchanged.
The offset is a depth-buffer value, not a position. The first version of this moved objects along world Z instead, and that cannot be made to work — the reasoning is worth keeping because it is the sort of thing that looks like a tuning problem right up until you do the arithmetic.
Two neighbouring sorting orders had to stay within a hair of each other, or a perspective camera would project the depth silhouette away from the sprite it belongs to and the occluded region would slide about as the camera moved. But a whole sorting layer holds a thousand orders, so a layer was then a thousand times that hair — over a world unit — and the parallax came back multiplied. Squeeze the other way and neighbouring orders fall inside a single quantisation step of the depth buffer, where they compare equal and X-Ray silently never triggers. There is no value satisfying both limits: a layer cannot be narrower than a thousand quantisation steps, and it cannot be wider than a hair.
Writing the depth value directly removes the whole dilemma. Nothing moves, so no projection can disagree about where it is; and the value is chosen in the buffer's own units, where the spacing is known rather than inferred through a projection matrix.
Everything Keyline writes sits behind the sprite plane. That is what keeps the rest of the scene out of it. Ordinary sprite shaders do not write depth, but they do test it, so anything Keyline leaves in the buffer is read by sprites that had nothing to do with the effect. Every offset here is positive — further from the camera than the fragment that produced it — and an ordinary sprite therefore always passes its LEqual test against it. Not by choosing careful numbers, but by construction: there is no occluder value an unrelated sprite can fail against.
Properties
public static int ActiveSlotCount { get; }propertystaticHow many distinct sorting positions are taking part in the 2D depth scheme right now.
More
Occluders and sprite outlines, counted once per position however many objects share it. This is what the depth band is divided between, so it is also what decides how much room each of them gets.
public static float ActiveGap { get; }propertystaticThe depth between two neighbouring positions, as a fraction of the room behind a fragment.
More
The number the whole scheme lives or dies by. Measured on one orthographic camera, 0.00096 worked and 0.00092 did not — so a scene wants this comfortably above that, and it falls as more positions join.
Methods
Depth offset for a sorting position: how far behind its own fragment this object is compared, as a fraction of the room available there.
More
Higher in the sorting order means drawn later means nearer the camera, so a higher rank gets a <em>smaller</em> offset. Get that backwards and X-Ray appears exactly where the object is visible, which is the one failure that looks deliberate.
A sorting position as one number, ordered the way the scene is: bigger means drawn later means nearer the camera.
More
Public because everything that takes part in the 2D depth scheme has to claim its position in KeylineRankTable2D, and it can only do that if it can name it the same way the table does.
public static int RankOf(Renderer renderer)methodstaticRank of a renderer, read straight from its sorting fields.
Depth offset for a renderer, read straight from its sorting fields.
Fields
public const float RankStep = 8e-6ffieldstaticGap between two neighbouring sorting positions, as a fraction of the room between a fragment and the far plane.
More
A fraction rather than a distance in buffer units, because how much room there is depends on the projection — see KeylineDepth2D.hlsl, which does the mapping.
Why it is eight times what it was. The original value was chosen against the quantisation of a 24-bit buffer, and by that measure it was enormously safe: a whole sorting layer apart came to some fifteen thousand steps. It was not enough, and the reasoning was wrong in its premise rather than its arithmetic — the depth test a fragment actually meets is not only the stored 24-bit value. Hardware keeps a coarse, per-tile summary of depth and rejects against that first, and a summary is by definition less precise than what it summarises. How much less is not specified, varies by GPU, and depends on how many tiles the geometry covers.
Measured, not guessed: an orthographic camera at the same distance drew X-Ray correctly when a layer's separation was 0.00096 of the buffer and drew nothing at 0.00092 — a four per cent change flipping it outright, which is the signature of a threshold being straddled rather than of a value being merely small. Zooming in fixed it too, at identical depth values, which fits a per-tile mechanism and nothing else. The step below puts a layer's separation at about 0.0074, roughly eight times the value that failed.
public const float BandFar = 0.09ffieldstaticWhere the lowest-ranked object sits, as a fraction of the room behind its fragment.
More
Everything sits between here and BandNear, and the band is kept as shallow as the rank capacity allows.
It was once 0.35, on the reasoning that width buys capacity for free because these values are only ever compared. That is true in a scene made only of sprites and false the moment a mesh is in it: the band is measured against the room behind the fragment, so a third of the depth range behind every sprite belonged to Keyline, and any ordinary geometry inside it drew over the outline while the sprite itself stayed in front. It was then cut to 0.035.
0.09 is the middle ground the measurement forced. The separation between layers had to grow eightfold to clear the hardware's coarse depth test — see RankStep — and the band has to be deep enough to hold the layers a real project defines at that step. A quarter of the old depth still keeps ordinary geometry out of the way in every mixed scene the demo has, with room above the eight or so sorting layers a 2D project typically defines.
public const float BandNear = 0.0002ffieldstaticWhere the highest-ranked object sits, as a fraction of the room behind its fragment.
More
Above zero on purpose. At zero the nearest occluder would share a depth value with an ordinary sprite drawn at the same place, and equality is exactly where LEqual and Greater disagree about who wins.
public const int OrdersPerLayer = 1024fieldstaticOrders reserved per sorting layer. Anything outside is clamped rather than allowed to spill into the neighbouring layer's range — a sprite compared at the wrong depth is a worse outcome than one compared at the edge of its own layer.
struct KeylineSourceMesh
FourSsets.Keyline · Runtime/KeylineOutlineController.Diagnostics.cs
A source renderer and the mesh it draws, as the diagnostics report them.
Fields
The renderer wearing it — named in the inspector so the child is findable.
public Mesh MeshfieldThe mesh itself, and the asset whose importer holds the switch.
class KeylineStencilPool
FourSsets.Keyline · Runtime/KeylineStencilPool.cs
Allocates unique stencil value pairs (primary + occluded companion) for outlines that are not in an KeylineMergeGroup. Both values live in bits 1–7; bit 0 is reserved for X-Ray companion hollow stamps.
More about KeylineStencilPool
The occluded value is the primary with bit 0 set, so the two are the same number everywhere bit 0 is masked off. That is deliberate and load-bearing: the overlap fence claims pixels on bits 1–7 and wants every pass of a unit to claim the <em>same</em> value, because "this unit has painted here" means one thing whichever pass wrote it. Splitting them into two distinct numbers was tried and cost half the pool while breaking the fence it was meant to sharpen.
Properties
public static int Generation { get; }propertystaticBumped every time the pool is emptied.
More
Anything holding an allocation needs this. With "Reload Domain" and "Reload Scene" both disabled the components are the same instances as before, so a held pair number survives a reset that the pool has forgotten about — and the next allocation hands the same number to somebody else. Two unrelated objects then share a stencil and merge into each other, which looks like anything but a stale integer.
Comparing one int on enable is cheaper than the afternoon that costs.
Methods
public static void EnsurePair(ref int primaryRef, ref int occludedRef, ref int generation)methodstaticTake a pair if this holder does not already have a valid one.
More
Three components hold pairs — merge groups, controllers allocating their own, and sprite figures — and each of them needs the same two guards: do not take a second pair when one is already held, and do not trust a number issued before the pool was last emptied. Written once here rather than three times, because the second guard is subtle enough that one of the three would eventually be written without it.
public static void ReturnPair(ref int primaryRef, ref int occludedRef, ref int generation)methodstaticGive a held pair back, if it is still the pool's current issue.
More
Releasing a number from a previous generation would free one that now belongs to somebody else — the same collision from the other direction.
Allocate primary + occluded stencil ids. Returns false if the pool is exhausted.
Returns a pair to the pool. Ignores values that are not a valid pair (even primary of at least 2, occluded equal to primary + 1). Does not check the generation; see ReturnPair.
Fields
public const int FullStencilMask = 255fieldstaticFull 8-bit stencil mask.
public const int PrimaryBitsMask = 254fieldstaticPrimary mask/outline bits (excludes X-Ray hollow bit 0).
public const int XRayHollowBit = 1fieldstaticShared bit stamped by X-Ray companion interior masks.
public const int FallbackPrimary = 64fieldstaticThe pair handed out when the pool is exhausted, and never handed out otherwise: the allocation loop skips this base. Before the reservation the overflow value was also a legitimately allocatable pair, so the 128th outline shared a stencil with whichever object had been issued 64 — their fences and merges bled into each other. Overflow objects still share this value with one another, which is the documented degradation; sharing it with a properly allocated unit was the bug.
public const int FallbackOccluded = 65fieldstaticThe occluded companion of FallbackPrimary.
class KeylineXRayOccluderBuffer
FourSsets.Keyline · Runtime/KeylineXRayOccluderBuffer.cs
Owns the global side of the X-Ray occluder buffer: whether a pass exists to fill it, the material that fills it, and — the part that matters most — what every camera reads when it does not.
More about KeylineXRayOccluderBuffer
The stale-global problem this exists to prevent. The buffer is bound as a global texture, and a frame holds more cameras than people remember: the game camera, the Scene view, reflection probes, material previews, a second camera for a minimap. Bind the buffer for one and say nothing for the others, and those others sample whatever the last camera left there. The symptom is an outline that shows through a wall in the Game view and not in the Scene view, or a rim that flickers as two cameras take turns — a bug that looks like the feature is broken when in fact it was never told what to think.
So there is no "unbound" state. A camera either gets a real buffer or gets FarFallback, a 1×1 texture meaning "nothing occludes anything anywhere", which reproduces the ordinary X-Ray exactly. Falling back to the old picture is always right; falling back to the previous camera's picture never is.
Public because the pipeline assemblies are separate ones. The URP feature and the HDRP custom pass live in FourSsets.Keyline.URP and .HDRP, and InternalsVisibleTo is granted only to the Editor assembly — deliberately, so the editor half can reach the machinery without any of it becoming API a buyer depends on. The pipeline halves need a genuine door, and this is it: one façade carrying exactly what a pass needs and nothing else, so KeylineShaderIds and KeylineXRayLayerSlots can both stay internal. It is the same answer KeylineSoftEdgeRegistry arrived at for the same reason.
Properties
public static bool PassInstalled { get; set; }propertystaticTrue while a render feature or custom pass is installed and recording.
More
Set by the pass itself every time it records, and cleared when the feature is disposed — the same handshake KeylineSoftEdgeRegistry.PassInstalled uses, for the same reason: a runtime assembly cannot ask a renderer asset what is on it, but a pass that is running can say so.
public static int BlockerFieldPass { get; }propertystaticPass index of the program that resolves this buffer into one channel for a given set of blockers. Drawn by the Soft Edge pass; see KeylineXRayBlockerField.hlsl.
public static bool Active { get; }propertystaticWhether the feature should do anything at all this frame.
More
Both halves are required and neither implies the other. No pass means nothing can fill the buffer; no narrowed mask means nothing would read it. Either way the answer is to spend nothing.
Where to get a white texture of the right shape for the pipeline in use.
More
Why the 1×1 below is not enough. The outline declares the buffer with TEXTURE2D_X, and on HDRP that expands to a texture array. Binding a flat Texture2D to an array sampler reads black — which this buffer means as "an occluder at the near plane" — so on HDRP the switched-off state clipped every X-Ray instead of restoring the ordinary one. SRP Core keeps a correctly shaped white texture; the HDRP half points at it, because TextureXR lives in a package this assembly does not reference.
A function, not a texture, and that distinction was itself a bug. The first version handed over the texture at load, from InitializeOnLoadMethod — before HDRP had finished bringing its own resources up. It captured null, the 1×1 was used instead, and switching the feature off changed nothing on HDRP. Asked for at the moment it is needed, there is no moment to be too early for.
public static Texture FarFallback { get; }propertystaticA texture reading "far" in every channel: nothing occludes, so nothing is blocked.
More
White because the buffer stores depth normalised so that larger is further, and 1 is as far as it goes. Bound to cameras the pass skips, and to every camera when the feature is switched off, so a sampler is never reading another camera's leftovers.
1×1 and Clamp: the read is a sample, not a load, so one texel answers every coordinate. A load would be out of bounds here and out-of-bounds loads return zero, which is the one value this buffer must never accidentally produce.
public static Material Material { get; }propertystaticThe material the prepass draws occluders with, one pass per channel.
public static GraphicsFormat OccluderFormat { get; }propertystaticThe format the occluder buffer is allocated in, and the one number that follows from it.
More
Thirty-two float bits a channel, because the tolerance is what a seam looks like. The test has to forgive the difference between two recordings of one surface — the buffer's and the fragment's — or a surface reads as being behind itself and shimmers. Sixteen normalised bits make that forgiveness coarse, and coarse in this space is not coarse in the world: the same tolerance is a couple of millimetres a few metres out and centimetres at twenty, because normalised depth is nonlinear. Along a line where an object meets a floor the two depths are within it, so the test answered "nothing in front of me" and the X-Ray rim drew a hairline through the floor.
A float target moves the tolerance three orders of magnitude down — fractions of a millimetre at the same distances — and the seam goes with it. It costs twice the memory and the bandwidth of the sixteen-bit target, and only while layer filtering is switched on at all.
Blending is the reason for the test rather than the format alone: the prepass keeps the nearest occluder with a Min blend, and blending into a 32-bit float target is optional hardware. Where it is missing the old format is used with the old tolerance, which is the picture this package shipped with.
public static float DepthEpsilon { get; }propertystaticThe tolerance that format is worth, in the buffer's own normalised depth.
More
Two quanta of the sixteen-bit target, or — where the float target is available — a value a few times its rounding error, which is relative and therefore tiny for every depth a scene contains.
public static int DepthEpsilonPropertyId { get; }propertystaticThe shader property the tolerance is written to, for the pipeline halves — they set it beside the buffer they allocate, and they cannot see KeylineShaderIds.
public static int ChannelCount { get; }propertystaticHow many channels the occluder buffer carries: the catch-all plus the named.
public static int OccludersTextureId { get; }propertystaticThe global texture name the outline shaders sample.
Layers that wanted a channel of their own and did not get one, so they were folded into the catch-all and now block every outline.
More
Surfaced so an inspector can say so. The fold is deliberately the safe direction — too little X-Ray rather than too much — but "safe" is not the same as "expected", and a silent one would read as the mask being ignored.
public static int NamedChannelCapacity { get; }propertystaticHow many layers may have a channel of their own before folding begins.
Methods
public static void BindFallback()methodstaticBind the fallback, so nothing downstream can read a previous camera's buffer.
Which layers a given channel collects, for the pass to filter its draw by.
The layers a channel should collect from the transparent queue, or zero when it should collect none — which is the answer for every project that has not named any.
More
Layer 0 is removed, and that is the safety property this method exists for. Keyline's own rim mirrors are created with new GameObject and never assigned a layer, so they are on Default whatever their source object is on. A mirror is the inverted hull — larger than the object and no further away than the rim it draws — so collecting one as an occluder makes the X-Ray clip itself and vanish. The opaque list keeps them out by queue; here the queue no longer helps, so the layer does. Naming Default in the mask cannot bring them back, because this is where the mask is read and Default never survives it.
One method rather than the same expression in two passes: the guarantee above is worth exactly as much as the number of places that can forget it.
namedLayers is passed in rather than held here, because the two pipelines keep it in different places — the renderer feature on URP, the build manifest on HDRP, since Keyline's project settings live outside Assets and a player cannot read them. A static to hold it would be one more piece of state to reset on a domain reload and one more chance for a pass to read the other pipeline's answer.
Events
Raised when the answer above changes.
More
Main Sees Through needs to hear it; X-Ray does not, and the difference is which way each falls back. An X-Ray outline keeps its hardware depth test and only ever clips further, so with no pass to fill the buffer it reads the "nothing occludes" fallback and draws exactly what it always drew. A Main outline with the feature on has handed its depth test to the shader — so the same fallback would let it draw in front of everything. It therefore only moves the test once a pass is actually recording, and this is how it learns that the moment arrived.
class KeylineXRayOccluderCustomPass : CustomPass
FourSsets.Keyline · Runtime/Pipelines/HDRP/KeylineXRayOccluderCustomPass.cs
Fills the X-Ray occluder buffer on HDRP — the same job KeylineXRayOccluderFeature does on URP, and deliberately the same shape as KeylineSoftEdgeCustomPass, which is how Keyline's other optional pass reaches HDRP. Hosted on a global CustomPassVolume created by the settings switch.
More about KeylineXRayOccluderCustomPass
What it draws. Occluder geometry, once per channel, into one RGBA target with BlendOp Min, so each channel ends up holding the depth of the nearest occluder on that channel's layers. Nothing is shaded: the override material writes depth and nothing else. Keyline's layer-slot resolver decides which layers share which channel, and the outline shaders read the result through one global texture.
Injected before transparents, because that is where every Keyline outline draws and where opaque geometry — the thing that occludes — has already been submitted.
Fields
Name of the GameObject holding the global Custom Pass volume that runs this pass; used to find existing volumes.
class KeylineXRayOccluderFeature : ScriptableRendererFeature
FourSsets.Keyline · Runtime/Pipelines/URP/KeylineXRayOccluderFeature.cs
Fills the X-Ray occluder buffer so outlines can be given per-object see-through layers.
More about KeylineXRayOccluderFeature
What it draws. Occluder geometry, once per channel, into one RGBA target with BlendOp Min. Each channel ends up holding the depth of the nearest occluder on that channel's layers, and Keyline's layer-slot resolver decides which layers share which channel. Nothing is shaded: the override material writes depth and nothing else.
Why one target and not one per mask. Layers that every narrowed outline sees through can veto nothing and are not drawn at all; layers no outline sees through are interchangeable and share the catch-all channel. Only layers the outlines actually disagree about need one of their own, and disagreement is rare — a scene where every outline uses the same mask needs zero named channels. That is what keeps the cost independent of how many different masks the project uses, which is the whole point of a per-object setting.
It costs nothing when unused. No narrowed mask anywhere means no texture is allocated and no list is drawn — see KeylineXRayOccluderBuffer.Active. The fallback is still bound, because a camera reading a stale global is worse than one reading a deliberate "nothing occludes".
Properties
Read by the settings page, which presents this and the HDRP store as one field.
Methods
Creates the render pass and reports the X-Ray occluder pass as installed to KeylineXRayOccluderBuffer.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)methodEnqueues the pass for every camera, passing on the transparent occluder layers.
class KeylineXRayOccluderHdrpBootstrap
FourSsets.Keyline · Runtime/Pipelines/HDRP/KeylineXRayOccluderHdrpBootstrap.cs
Keeps a hidden global Custom Pass volume alive whenever the buyer has switched X-Ray Layer Filtering on, so no scene ever has to hold one.
More about KeylineXRayOccluderHdrpBootstrap
Project-wide, in both modes, and that took two halves. This class covers play mode and player builds, from Boot. Edit mode has no equivalent entry point — nothing runs at "the editor is now rendering the Scene view" — so an editor-side keeper calls EnsureVolume on a slow tick instead. Between them the switch behaves the way URP's does: a project-wide setting with nothing added to any scene.
Deliberately the same shape as KeylineSoftEdgeHdrpBootstrap. The two are the same problem twice and a buyer who has learnt one has learnt the other.
Properties
public static bool HasHiddenVolume { get; }propertystaticWhether a hidden volume of ours is currently alive.
public static bool HasVolume { get; }propertystaticWhether any volume is alive to run the pass — the honest "is it working" answer.
public static int SavedVolumeCount { get; }propertystaticVolumes a pre-1.2 Keyline saved into the buyer's scenes, which are now surplus.
Methods
public static void SupplyFallback()methodstaticTell Keyline where to find the white texture HDRP's own samplers expect.
More
This matters most when the pass is not installed. TEXTURE2D_X is a texture array on HDRP, so the plain 1×1 the runtime keeps for URP reads black through an array sampler — and black means "occluded" here. Without this, switching the feature off left every X-Ray clipped.
A pointer to the texture, not the texture. Handing the object over at load captured whatever TextureXR had at that instant, and from InitializeOnLoadMethod that is before HDRP has finished starting: null, silently, and the 1×1 was used after all. The function is called when the fallback is actually wanted, by which time HDRP is up.
Public for the same reason everything else here is: the editor installer that also calls it lives in FourSsets.Keyline.Editor.HDRP, a different assembly, and internal does not cross that line.
public static CustomPassVolume EnsureVolume()methodstaticA volume exists and is running the pass, whatever it takes to get there.
More
Called from three places that must not tread on each other: once at startup, once when the settings switch is turned on, and on a slow tick in edit mode. So it is written to be safe to call at any moment and cheap to call repeatedly, and it never creates a second volume — that was the original bug and it produced a prepass running twice.
public static void Deduplicate()methodstaticMake sure only one volume is running, and hold on to the survivor.
More
Called on a slow beat rather than from EnsureVolume, because it is the one operation here that has to search: EnsureVolume earns its speed by trusting the reference it holds, and trusting it is exactly what cannot notice a second volume arriving with a scene. KeylineHdrpVolumes.Deduplicate has the case.
Take the hidden volume away, leaving anything the buyer made themselves alone.
More
thorough searches as well as using the held reference. The keeper passes false on its ordinary tick, where this runs constantly while a pass is off and must cost nothing, and true on the slow sweep — which is the only thing that can find a hidden volume nothing holds a reference to any more.
public static int DestroyVolumes()methodstaticTake every Keyline X-Ray volume away, including any saved into the open scenes. Returns how many there were, which is what the Off message reports.
More
Only ever from an explicit Off or Remove. The keeper uses DestroyHiddenVolume instead, because deleting a GameObject a buyer put in their scene — even one of ours — is not something to do on a timer.
The held volume is passed in, not left to the search. Written that way after switching the pass off stopped working: the search missed the hidden volume, this method destroyed nothing, and Forget then threw away the one reference that could still reach it. The pass carried on running with nothing able to stop it. Handing the reference over costs a parameter and removes the whole class of failure.
Nothing matches. Try fewer words, or another spelling (the search also knows colour, hide, raycast, x-ray and similar).