SLT (Sobel Line Texture) is an ink-line renderer for Unreal Engine 5 where the artist decides where lines go, in the material, instead of the renderer guessing from normals or object IDs. Materials write mask data into two extra GBuffer-style render targets during the base pass. A plugin runs edge detection on those masks and on scene depth, turns the edges into a distance field, and resolves clean anti-aliased lines of any width. A post-process material darkens the image with that line texture before depth of field, so the ink is defocused by the engine's real bokeh. Stock engine code is touched in three small places: the two buffers, eight material output pins, and one override in the DoF setup. Everything else is a self-contained plugin built on RDG and a scene view extension.
At a glance
Lines are extracted from authored masks and inked before DoF
One frame, top to bottom. The two side boxes feed the main column: translucency passes add glass data to Detect, and the line-depth pass carries the silhouette's near depth into the engine's DoF so the ink defocuses with the object it outlines.
What the artist authors
The artist paints "where lines should be" into the material, the same way you would wire Custom Depth. Two RGBA8 targets, SLTBufferA and SLTBufferB, give eight channels. Six are line sources, two are controls. The sources form three families, each a pair of channels that cross-fade so an asset can dissolve from a detailed line set to a simpler one with distance.
| Channels | Role | What the artist writes | How the detector reads it |
|---|---|---|---|
| A.R, A.G | Mask lines (pair) | Mid grey with black and white patches | Only a black-to-white border draws. Grey-to-white and grey-to-black are culled. Freestanding, open lines. |
| A.B, A.A | Area lines (pair) | Stepped values about 0.1 apart | Every step outlines a region. Closed outlines, the fast way to outline an area. |
| B.R, B.G | Shader lines (pair) | Same convention as mask lines | Same gate and same ink strength as mask lines. |
| B.B | Control: pair cross-fade | A 0 to 1 value per pixel, usually depth-driven | The A side of every pair dissolves out as the B side dissolves in. Applied to resolved coverage, never to the masks, so it is a cross-fade and not a shape morph. |
| B.A | Control: line fade | A 0 to 1 value per pixel, 0 = no fade | Published raw. The material applies it per line type. Depth lines ignore it so silhouettes survive at distance. |
| Scene depth | Depth line | Nothing, it is free | A log-dampened Sobel on depth gives the object silhouette. |
Masked materials need nothing extra: they run in the opaque base pass and write all eight channels. Translucent materials never write line sources. They get two abilities of their own, described under stage 8 below.
The passes, stage by stage
Each stage opens with a plain sentence, then the technical detail. Stages 2 to 6 are RDG passes in the plugin's scene view extension, run in the pre-post-process hook before DoF and before TSR.
- Base pass: authoring. The buffers are filled while the GBuffer is written, at no extra pass. Technically:
SLTBufferA/Bare extra MRTs bound in the classic base pass and in Nanite shading, cleared to the mid-grey resting value, allocated on the deferred path only behind a master cvar. - Detect. One full-screen pass finds every edge, once. Technically: a 5x5 Sobel plus non-maximum-suppression thinning over the six source channels, the log-dampened scene depth, and the translucent silhouette coverage target. It writes packed seed textures and border-normal angles. The gates differ per family on purpose. The mask gate ramps around a threshold so grey borders drop out. The area gate ramps from zero so a small step still fires.
- Jump flood. Every pixel learns how far it is from the nearest line. Technically: a jump-flood algorithm with sub-texel seed centroids, a halving step sequence, and two corrective passes to fix the non-nearest-seed errors standard JFA leaves. Only three fields are needed, because all pairs share one cross-fade value: state A (the union of the pair first halves), state B (the second halves), and depth. States A and B pack into one RGBA16 and flood together. Depth floods in an RG16. The reach is floored at what the current width needs, so the flood never runs longer than the line requires.
- Resolve. The distance becomes an anti-aliased line with a pen width and a feather. Technically: coverage is a smoothstep of the perpendicular distance to the line through the nearest seed, a capsule rather than a point distance, which removed the scalloping between seeds. Width is defined in reference pixels at 1080p, so it is resolution independent and costs the same at any size. Ink strength is the crest strength of the ridge, read as a 3x3 max at the seed. Pair cross-fade and fade are sampled at the seed, so they stay correct where a line spills off its mesh onto the background. Output is one RGBA texture: R mask, G area, B shader, A depth line, deliberately unfaded. A second texture,
SLTLineControl, carries the fade and the glass fades sampled at the seed, plus a composited depth in metres. - Line depth and DoF inheritance. A silhouette line half sits on the near object and half on the far background. Without help, the outer half would blur at the background's focus distance. Technically: a small pass writes the near-side depth at depth-line pixels. The DoF setup shader takes the minimum of scene depth and line depth when computing the circle of confusion, so the whole line defocuses with the object it belongs to. The same override is mirrored in the DoF recombine pass. This is the one deliberate hook inside stock DoF code.
- Publish. The plugin hands the textures to the material graph with no translator changes. Technically:
SLTLinesandSLTLineControlare registered as named User Scene Textures on the view family, so a post-process material reads them with a User Scene Texture node. - Composite, in a material. A small post-process material at Scene Color Before DOF does the darkening. Technically: it composites the four line types, applies the fades by hand, and optionally smudges the plate under the ink. Because it runs on linear HDR before DoF and before the tonemapper, the ink gets real bokeh and rolls off naturally through the tonemap. The painterly Kuwahara chain runs at the same location at lower priority, so lines land on top of the strokes. The plugin's own composite at Tonemap is debug only and is off by default.
- Translucency. Glass and smoke get exactly two abilities. They can fade ink behind them, and flagged meshes get their own silhouette line. Technically: a dedicated mesh pass modeled on the Distortion pass writes a transmittance-accumulated fade mask and a min-blended glass depth, which the resolve publishes into the control texture. A second pass writes additive coverage that Detect unions into the depth-line seed, so overlapping glasses keep both outlines. Translucents never write the line-source channels and never touch scene depth, so authored data behind glass is never destroyed and DoF focus is never corrupted.
Engine footprint
The fork merges upstream, so stock engine edits are kept to what a plugin cannot do. Everything else, including all pass code and about forty r.SLT.Lines.* console variables, lives in the SLTRenderer plugin.
| Area | What changed in stock engine code | Why it cannot live in the plugin |
|---|---|---|
| The two buffers | Allocate SLTBufferA/B in the scene textures and bind them as base-pass MRTs, Custom Depth style, in the classic base pass and Nanite shading | A plugin cannot add members to the scene textures or bind extra base-pass targets |
| Material pins | Eight material outputs through the material attributes, the HLSL translator and the material template | Material attributes are a fixed engine enum |
| Translucency passes | Two mesh passes, fade and silhouette coverage, in self-contained new files plus small registration edits | Mesh passes are registered engine-side |
| DoF inheritance | Minimum of scene depth and line depth when computing the CoC, in DoF setup and recombine | A scene view extension has no hook into the DoF setup |
The User Scene Texture handoff needed no translator change. A dedicated SceneTexture enum entry was considered and rejected as ten more merge-conflict touch points.
Known limits
- Platform scope. Win64, SM6, deferred only. The buffers are not allocated on the forward or mobile paths.
- Temporal. The SLT buffers are not temporally resolved. A temporal accumulation pass was built, gave only ghosting, and was removed. The line passes run before TSR, so TSR does see the ink.
- Cost. Not profiled in milliseconds. The design figures are tap counts. The flood dominates and is bounded by the reach, which is floored at what the current line width needs.
- Per-type overlap. The three pair families share one union field per state. Where two types run within a line width of each other, the nearer seed's type wins pixel by pixel. Apart, they are exact.
- Hair strands do not write the buffers, so grooms get the depth line only. Hair cards work normally.
- MRT budget. Two extra targets do not fit alongside static lighting's seven-target layout. A third buffer was planned and dropped for this reason.
- Translucent fade is symmetric. Smoke behind a window dims the window's outline as much as the reverse. Accepted, reads as atmosphere.
- DoF must be on for the ink to defocus. With DoF off the path degrades to a sharp darkened line.