Install

After purchasing on Fab, open the Epic Games Launcher → Unreal Engine → Library → Fab Library, find Ultimate Celshading PBR and click Install to Engine and choose your engine version. The launcher installs it into the engine as a ready-to-use plugin, so every project on that engine can enable it.

  1. Open your project.
  2. Edit → Plugins → Rendering → enable Ultimate Celshading PBR.
  3. Restart the editor when prompted.

Using a source-built engine? Copy the UltimateCelshadingPBR folder into your project's Plugins/ folder instead; the plugin compiles with the project. Full C++ and shader source is included. A copy there stays off until you enable it in Edit → Plugins, the same as the engine install.

Packaging a Blueprint-only project: a project that enables a code plugin gets a small C++ executable of its own when you package it, so the packaging machine needs Visual Studio 2022 with the Game development with C++ workload. The editor works without it.

Demo maps

The plugin ships two maps. Fab installs the plugin into the engine, so in the Content Browser open Settings and tick both Show Engine Content and Show Plugin Content (a copy in your project's Plugins/ folder needs only the second), then UltimateCelshadingPBR Content → Maps:

  • Showcase — ten labelled feature stations: PBR with fog, colored lights and volumetrics · color-aware outline · halftone artistic fill · rim light · band gradient dot mask · band gradient strokes mask · a zone volume that blends fog, sun and sky light · full-screen mode · multi-layer stencil (several slots, each with its own style, in one view) · cel-shaded metallic and emissive surfaces.
  • Showcase_Presets — the five included presets side by side (Anime, Strokes, Manga, Comix, Black Shade), plus a row of further variations.

Every volume in these maps is an ordinary ACelshadingVolume: select one to see which overrides and preset produce the look. Both maps use movable lights only, so they open and play without a lighting build.

Project setup

The plugin gates pixels via the standard UE Custom Depth-Stencil pipeline.

Auto-configured. The plugin automatically promotes r.CustomDepth to mode 3 ("Enabled with Stencil") on module startup if your project hasn't set it already — so a fresh install just works. To make it permanent (or set it manually), add the following to DefaultEngine.ini under [/Script/Engine.RendererSettings]:
[/Script/Engine.RendererSettings]
r.CustomDepth=3

Equivalent UI path: Project Settings → Rendering → Postprocessing → Custom Depth-Stencil Pass = Enabled with Stencil.

Why Custom Depth? It's the only built-in mechanism that lets the plugin recognize "which actors should be cel-shaded" without forking the engine or modifying your materials.

Per-actor opt-in

For each actor that should be cel-shaded:

  1. Select the actor in the level (or its StaticMeshComponent / SkeletalMeshComponent).
  2. Details → Rendering:
    • ☑ Render CustomDepth Pass
    • CustomDepth Stencil Value = 1 (matches default r.Celshading.CelStencilRef)

That's it. The actor's existing material — DefaultLit, Subsurface, Cloth, Hair, anything — is preserved. The pass quantizes its lighting and adds outline + rim on top.

Stencil layers

The plugin uses the full 8-bit Custom Stencil byte — pick any value 1..255 for your StencilSlot and set the matching CustomDepth Stencil Value on the mesh component. There is no fixed slot table and no maximum layer count: place as many independent volumes as you need, each driving its own slot.

Internally, the registry stores a packed list of active per-slot snapshots — slots without a volume cost zero GPU time (no dispatch). So if you only have one slot in use, you only pay for one dispatch, regardless of which slot number you chose (e.g. StencilSlot=42 works the same as 1).

Multiple volumes, same slot? Their styles blend by BlendPriority + ContributionWeight, just like APostProcessVolume.

Need every visible mesh cel-shaded without per-actor setup? Set bAffectWholeScreen = true on a volume (or r.Celshading.AffectWholeScreen 1 globally) — the stencil gate is bypassed entirely. See the Stencil & debug table below.

Volumes & blending

Drop an ACelshadingVolume into the level. By default bUnbound = true — the volume applies globally. To zone it, untick bUnbound, scale the box, and set SpatialFalloff for soft falloff outside the box.

Volume properties

  • StencilSlot [1..255] — which CustomDepth Stencil Value this volume drives.
  • PatternDepthReference — optional Actor pointer. When set, the camera-to-actor distance is used as a single depth pivot for this volume's Halftone + Band Mask cell pitch: pattern density stays uniform across the entire stencil slot regardless of surface curvature (zero strobe). When unset, the patterns are pure screen-fixed (cell pitch in actual pixels). See Pattern Depth Reference below.
  • bUnbound — global mode. Box is hidden.
  • SpatialFalloff (cm) — fade distance outside the box. Smoothstep falloff.
  • ContributionWeight [0..1] — manual multiplier on top of spatial weight.
  • BlendPriority — overlapping volumes sort ascending; higher priority writes on top (matches APostProcessVolume::BlendPriority).

Style overrides

Each Style field has a paired bOverride_X bit — only flagged fields participate in the per-layer blend. Non-flagged fields fall back to the global r.Celshading.* CVars. A volume with no overrides skips Style entirely (no snapshot pushed).

Lighting overrides

Same per-field bOverride_* pattern for Sun / Sky / Fog. Affects the lights the plugin auto-discovers in the level: the ADirectionalLight flagged Atmosphere Sun Light (or the first directional light when none is flagged), the first ASkyLight and the first AExponentialHeightFog. The scene's own values are captured as the base and restored when the camera leaves every overriding volume. If you edit those actors manually while volumes are present, call UCelshadingSubsystem::RefreshBase() to re-capture the base.

Pattern Depth Reference (sprite-anchor)

By default, Halftone and Band Mask (Dots / Strokes) cells are sized in screen pixels — the same density covers the screen regardless of object distance. For hero meshes you usually want the dots / strokes to behave like a billboard sprite locked to the actor's pivot: the pattern grid tracks the actor's screen position and zooms in/out from its centre as the camera moves toward or away from it. Set the volume's PatternDepthReference to any Actor (typically the hero itself, or a helper pivot in the centre of an object cluster):

  • Unset (default) → pure screen-fixed. Cell pitch = HalftoneScale / BandDitherScale in actual pixels. Same density everywhere on screen at any distance. Best for stylised overlay looks.
  • Set to an Actor → sprite-anchored:
    • Anchor: the actor's projected screen-space position becomes the cell-grid origin — the pattern stays locked to the actor as the camera orbits or pans, no swimming.
    • Zoom: cell pitch is divided by dist(camera, RefActor) / 500. As you approach the actor, dots zoom outward from its centre exactly like a billboard sprite getting closer to camera; as you retreat, the pattern shrinks back toward it.
    • A single set of values (anchor + scale) is applied to every pixel of this volume's stencil slot, so pattern density stays uniform across the whole object/cluster — zero strobe regardless of surface curvature.

Reference distance 500 cm: at that range the effective cell pitch matches the configured Scale exactly. If multiple volumes share the same StencilSlot and each provides a reference actor, the final anchor is a weight-blended average position across all contributing volumes (same blend weights as the rest of the per-field overrides).

When the reference actor moves behind the camera, sprite-anchor mode silently falls back to pure screen-fixed for that frame — no clip-space artefacts.

Why a sprite anchor, not per-pixel SceneDepth? Per-pixel depth varies across curved surfaces and that variation amplifies through the cell-grid frac() phase into severe pattern strobing. Anchoring the grid to a single projected actor position means every pixel of the dispatch shares the exact same anchor + divisor — strobe-free by construction, and the pattern "feels" attached to the object the way a comic-book screentone is glued to a panel character.

Outside Outline (anime halo)

Two outline systems run independently:

  • Inside Laplacian outline (OutlineThicknessNear/Far) — adaptive kernel that detects creases / depth jumps INSIDE the silhouette. Pixels near silhouette get darkened toward OutlineColor; concave creases get brightened toward SpecularHighlightColor.
  • Outside Outline / halo (OutsideOutlineThickness) — solid anime-style halo that wraps AROUND the silhouette over the background. Pixels within N px of a stencil pixel get the colour of the closest cel-shaded silhouette pixel written out — so the halo carries every internal effect (band quantization, brighten blik, rim glow, fog) as a continuous extension of the inside line.

To enable: set OutsideOutlineThickness to 1..32 px on the volume (or globally via r.Celshading.OutsideOutlineThickness). The plugin dispatches a second compute pass per stencil layer that has the halo enabled, reading the cel-output as input and dilating it outward. Pixel-perfect colour match with the inside silhouette.

Zero GPU cost when off. When OutsideOutlineThickness < 1 the second pass is not enqueued at all — only the regular cel pass runs. Use freely on layers that don't need it.

Typical values: 2..4 px for thin anime line, 5..8 for thicker comic outline. Cost scales O(N²) per halo pixel due to dilation kernel scan; use modest radii in production.

Tint By Light Direction (color of light)

Cel-shaded surfaces can transmit the color of the light that hits them — a yellow character lit by a warm sun reads warm-yellow on the lit side and cool on the shaded side (instead of a flat BaseColor × dim shadow). The trick: the cel pass scales the quantized brightness by the SceneColor's color direction (per-pixel hue), not by the BaseColor chroma. Result: light hue propagates through quantization, color story survives the toon math.

Toggle: Volume → Cel Look → Color Response → Tint By Light Direction (or CVar r.Celshading.TintByLightDirection). Default: on.

When you want it

  • Multi-light scenes (warm sun + cool skylight, lanterns + moonlight) — surfaces blend between the lights' hues smoothly per-pixel.
  • Sunset / sunrise / mood lighting where the color of the light is part of the art direction, not just the surface color.
  • Atmosphere overrides via ACelshadingVolume — per-zone Sun/Sky color changes show up on every cel-shaded surface within the volume.

When to disable it

  • Pure flat-color comic / Saturday-morning-cartoon look, where you want surfaces to read as their flat BaseColor regardless of which light hits them. Set Tint By Light Direction = false — the pass falls back to MaxToon × BaseColor.chroma (BaseColor owns all the color).
  • If you specifically want every band as one solid hue with no per-pixel variation (Black-Shades / Hard-Cel preset territory).

Trade-off you should know

"Color direction" is read per-pixel from SceneColor. Where SceneColor saturates HDR-bright (specular hotspots), the direction smoothly shifts toward white — in early plugin builds this read as a soft bloom-like gradient on top of the cel bands. The current pass automatically fades the tint toward neutral white above HighlightCutoff, so spec hotspots stay flat-cel even with Tint By Light Direction on. No setup required.

Companion controls (Color Response group)

  • Banded Color Strength — quantizes the tint into BandCount color steps with a smoothstep transition (uses BandEdgeSoftness from the cel-bands block, so cel-edges and color-edges share the same softness feel). 0 = smooth tint, 1 = full hard-banded color zones (classic comic-book color flat).
  • Base Color Influence — mixes the surface BaseColor's chroma into the light-direction tint. 0 = pure light-direction tint (a white sun on a yellow surface produces a white lit band — full color-of-light effect). 1 = pure BaseColor chroma on lit pixels (surface hue dominates regardless of light colour). Use 0.2..0.4 to keep a hint of surface hue under intense or saturated lighting without abandoning the color-of-light story (avoids the lit band reading as a flat white-out under bright directional lights). CVar: r.Celshading.BaseColorInfluence.
  • Lighting Desaturation — pulls the final tint toward neutral white. 0 = full color story, 1 = pure greyscale lit (kills the trick entirely without flipping Tint By Light Direction off).

Material setup

Nothing special required — Tint By Light Direction reads the standard Lit GBuffer's SceneColor + BaseColor. Works with every UE shading model the plugin already supports (DefaultLit, Subsurface, SubsurfaceProfile, Cloth, Hair, Eye, ClearCoat).

The only material-side gotcha: if your material outputs a very HDR-bright Emissive (e.g. Emissive = BaseColor × 50) AND the surface is also Lit, the Emissive contribution dominates SceneColor and the tint reads as the Emissive's color rather than the lighting's color. Either tag emissive zones via the Ambient Occlusion marker (so the cel pass routes them through the Emissive path) or keep Emissive intensity moderate.

Presets

UCelshadingPreset is a DataAsset bundling Style + Lighting overrides. Two ways to create:

  1. Content Browser → Right-click → Miscellaneous → Data Asset → Celshading Preset.
  2. From a tuned volume: Save As Preset… button on the volume's Details panel.

Apply: assign the preset to the volume's Active Preset field, then click Apply Preset. This is a one-shot copy — subsequent edits to the preset don't auto-propagate. Re-click to update.

Sharing the stencil byte

CustomStencil is a single 8-bit value shared engine-wide. To coexist with other plugins that use it (engine outline, gameplay highlight, etc.), use a single bit instead of a full value:

r.Celshading.CelStencilMask 128    # check bit 7 (0x80) only
r.Celshading.CelStencilRef      128    # cel-shade when bit 7 is set

Then set CustomDepth Stencil Value = 128 on cel-shaded actors. The lower 7 bits remain free.

Console variables

Style fields are mirrored as r.Celshading.* console variables. CVars provide the global default when no volume overrides them. All of them are listed below; in the editor console, DumpCVars r.Celshading. -showhelp prints their live values with the help text.

Core

NameDefaultDescription
r.Celshading.MasterEnable1Master switch. 0 disables the entire pass.
r.Celshading.MasterStrength1.0Final blend amount (0 = scene unchanged).
r.Celshading.BandCount3Number of lighting bands (3 = shadow / mid / highlight; 2 = binary shadow/light).
r.Celshading.HighlightCutoff3.0Lighting magnitude mapped to the brightest band.
r.Celshading.OutputBrightness1.0Final output multiplier.
r.Celshading.FillLightStrength1.0Pre-quantize light gain.
r.Celshading.MinBaseColorBrightness0.03BaseColor floor when extracting lighting.
r.Celshading.FogAware1Reconstruct + reapply fog around quantization.
r.Celshading.LitDetail00..1 — preserve PBR detail on lit pixels (1 = "PBR highlights + crisp toon shadows" hybrid; 0 = pure cel).

Shadows

NameDefaultDescription
r.Celshading.ShadowCurve2.2Pow exponent before quantization. >1 crushes blacks.
r.Celshading.ShadowFloorBrightness0.022Dark band brightness as fraction of HighlightCutoff.
r.Celshading.InterBandContrast1.0Multiplier between bands.
r.Celshading.ShadowEntryPoint0.30Hard cutoff: lighting below this collapses to ShadowFloorBrightness.
r.Celshading.HighlightEntryPoint0.70Hard cutoff: lighting above this saturates to HighlightCutoff.
r.Celshading.BandEdgeSoftness0Anti-alias band edges (raise to ~0.02 to soften TAA jitter).
r.Celshading.SuperBlack00..1 — hard-ink "super-black" shadows: crushes toon shadows below SuperBlackThreshold to pure black, independent of ShadowFloorBrightness. 0 = off.
r.Celshading.SuperBlackThreshold0.30Shaped-lighting level (0..1) below which SuperBlack applies. Same space as ShadowEntryPoint.

Outline (adaptive Laplacian)

NameDefaultDescription
r.Celshading.OutlineStrength1.0Final outline intensity.
r.Celshading.OutlineDepthScale1.0Multiplier on depth Laplacian.
r.Celshading.OutlineNormalScale1.0Multiplier on normal Laplacian projection.
r.Celshading.InternalLineThreshold1.0Depth knee (cm) — filters texture noise.
r.Celshading.OutlineThicknessFar1.0Kernel diameter (px) at far depth. Soft cap 32 — type any value; shader saturates beyond 32.
r.Celshading.OutlineThicknessNear3.8Kernel diameter (px) at near depth. Same soft cap 32. Cost is O(N²).
r.Celshading.OutsideOutlineThickness0Outside silhouette outline (anime halo) thickness in pixels. 0 (default) = disabled (zero GPU cost — second pass not dispatched). >0 = halo of N pixels drawn AROUND the actor's silhouette, sampling the cel-shaded silhouette pixel itself so all internal effects (band quantization, brighten path's SpecularHighlightColor blik, rim glow, fog) carry over. Soft cap 32. Independent from OutlineThicknessNear/Far (Laplacian inside outline for creases).
r.Celshading.OutlineDepthNear500Distance (cm) where kernel reaches OutlineThicknessNear.
r.Celshading.OutlineDepthFar3000Distance (cm) where kernel reaches OutlineThicknessFar.
r.Celshading.ColorAwareOutline0Tint outline by underlying BaseColor — red surfaces produce dark-red lines, blue surfaces dark-blue (anime self-shadow line look).
r.Celshading.OutlineColorAwareness0.7Blend amount: 0 = plain OutlineColor, 1 = OutlineColor × BaseColor. Active only when ColorAwareOutline is enabled.
r.Celshading.OutlineFillMode00..1 — blend between TINT (multiplicative, 0) and FILL (the line replaces the pixel with OutlineColor, 1).
r.Celshading.InvertOutline0Flip outline polarity: silhouette edges brighten, the crease highlight darkens to OutlineColor.

Halftone (manga screentone)

NameDefaultDescription
r.Celshading.HalftoneEnable0Master toggle for the halftone overlay. Off = entire block GPU-branch-skipped.
r.Celshading.HalftoneScale8.0Cell pitch in pixels. Smaller = denser dot pattern.
r.Celshading.HalftoneAngle45.0Grid rotation in degrees.
r.Celshading.HalftoneThreshold0.45Brightness at which dots peak. Pixels darker than this are fully dotted.
r.Celshading.HalftoneSoftness0.15Softness of the brightness-to-dot-size ramp.
r.Celshading.HalftoneStrength0.7Overlay opacity (0 = invisible, 1 = full dot color replacement).

Band Mask (Comix Inspired)

The legacy BandDither* CVar names are kept for backwards compatibility — the user-facing name is now Band Mask (DOTS / STROKES modes via StretchX + Angle).

NameDefaultDescription
r.Celshading.BandDither0Master toggle for the Band Mask comic ink layer. Modifies band assignment via per-pixel brightness bias.
r.Celshading.BandDitherScale4.0Cell pitch in pixels. Smaller = denser pattern.
r.Celshading.BandDitherAngle45.0Grid rotation in degrees. 45° = classic comic ink direction (default for STROKES mode).
r.Celshading.BandDitherStrength1.0Mask intensity (0 = clean band edges, 1 = max comic ink along transitions).
r.Celshading.BandDitherStretchX1.0Anisotropic stretch on the cell. 1 = DOTS (round halftone). 4..16 = STROKES (elongated comic ink strokes — combine with BandDitherAngle to rotate).
r.Celshading.BandDitherSoftness0.5Edge softness of the dot/stroke (0..1). 1 = soft halftone falloff; 0 = hard pixel-precise edge for crisp comic ink. Independent from BandEdgeSoftness, which softens the toon-band quantization edges.

Color response

NameDefaultDescription
r.Celshading.TintByLightDirection1Keep the color of the light (sun / sky tint) on toon pixels. 0 = legacy chroma-from-extracted-lighting path. See Tint By Light Direction.
r.Celshading.BandedColorStrength00..1 — hard banding of the light tint. 0 = smooth, 1 = quantized into BandCount steps per channel.
r.Celshading.BaseColorInfluence00..1 — mix the surface BaseColor's chroma into the light tint. 0 = pure color of light, 1 = pure surface hue.
r.Celshading.LightingDesaturation00..1 — desaturate the extracted lighting before quantization. 0 = keep the light chroma, 1 = greyscale.

Rim light

NameDefaultDescription
r.Celshading.RimLightIntensity0.8Screen-space rim intensity. On out of the box; 0 = off (GPU branch skipped). Typical range 0.5..30.
r.Celshading.RimFollowsSunDirection11 = the rim follows the directional light projected into view (RimLightAngle is an offset from the sun). 0 = camera-anchored (RimLightAngle is an absolute view-space angle).
r.Celshading.RimLightAngle325Rotation of the rim source in degrees (0..360) — which side of the silhouette glows.
r.Celshading.RimLightSpread0.87Width of the rim arc (0..1).
r.Celshading.RimLightHardness4.3Power applied to the rim mask. 1 = linear, higher = sharper falloff.
r.Celshading.RimLightSharpness0.070 = smooth gradient; above 0 = hard step for a flat toon rim band.
r.Celshading.RimLightFresnelExp2.0Fresnel exponent. Higher = rim concentrated at grazing angles.
r.Celshading.RimLightCameraInfluence1.00..1 — 1 = full Fresnel, rim only at silhouettes; 0 = "painted-on" rim that does not shift as you orbit.
r.Celshading.RimLightOutlineMix00..1 — gate the rim by the outline mask. 1 = the rim fires only where the line-art does. No extra cost.

Rim band dither

NameDefaultDescription
r.Celshading.RimDitherStrength00..1 — with Band Mask on, break the rim's fade-out zone into the same dot/stroke pattern. 0 = solid rim.
r.Celshading.RimDitherSolidEdge0.20..0.5 — fraction of the rim nearest the silhouette that stays solid.
r.Celshading.RimDitherSoftness00..1 — 0 = crisp dots and a sharp solid edge (comic ink), 1 = soft halftone falloff.

Metallic

NameDefaultDescription
r.Celshading.MetallicLightCompensation1Metallic pixels get a grazing-angle boost and a lifted dark band so metal never crushes to black.
r.Celshading.MetallicShadowFloor0.08Dark-band floor for metal, as a fraction of HighlightCutoff. Raise toward 0.25 for stronger shape readability.
r.Celshading.MetallicHighlightIntensity2.0Multiplier on the metallic specular highlight. 0 = no extra highlight.
r.Celshading.MetallicLitMultiplier2.0Multiplier on the lit zone of metal. 1 = neutral, 0 = flat metal.

Emissive

Unlit surfaces are always kept out of the bands (no quantization, outline or rim). Lit materials can mark glowing areas two ways: route 1 − EmissiveMask into the material's Ambient Occlusion output (the AO marker), or lower EmissiveBrightCutoff so very bright pixels are detected automatically.

NameDefaultDescription
r.Celshading.EmissiveAOCutoff1.0AO-marker threshold: pixels whose material AO is below this value are treated as emissive (AO = 1 means no marker).
r.Celshading.EmissiveBrightCutoff100Brightness (max RGB channel) above which a pixel is treated as emissive. 100 = effectively off; ~1.5..2 catches glowing areas on lit materials.
r.Celshading.EmissiveRampSoftness0.3Width of the brightness-detection ramp. Larger = softer toon-to-emissive transition.
r.Celshading.EmissiveMultiplier1.0Brightness multiplier on preserved emissive pixels. 2 = glows twice as bright.
r.Celshading.EmissiveDarkBoost0.850..1 — lift dark bands on emissive pixels so glowing surfaces read as self-lit even in shadow.

Stencil & debug

NameDefaultDescription
r.Celshading.AffectWholeScreen0Bypass stencil + depth gates and cel-shade EVERY visible pixel — no per-actor CustomDepth setup required. Per-volume override via bAffectWholeScreen.
r.Celshading.CelStencilMask255Bit mask applied to CustomStencil before comparing.
r.Celshading.CelStencilRef1Expected (CustomStencil & mask) value.
r.Celshading.GateOnDepthMatch1Also require CustomDepth ≈ SceneDepth (occlusion-safe).
r.Celshading.DepthGateTolerance0.005Depth-gate tolerance as a fraction of scene depth (0.005 = 0.5%, distance-invariant). The cel pass skips a stencil pixel that sits farther than this behind the front-most opaque surface.
r.Celshading.DiagnosticView00 = final, 1 = defogged color, 2 = reconstructed fog, 3 = extracted lighting, 4 = outline mask (R = darken, G = brighten), 5 = rim mask.

Blueprint API

UCelshadingSubsystem

  • RefreshBase() — re-discover Sun/Sky/Fog and re-snapshot base on next tick. Call after manually editing the scene's lights/fog.

ACelshadingVolume

All three are also buttons in the volume's Details panel (Preset group).

  • ApplyPreset() — copy ActivePreset's Style + Lighting into this volume (overwrites the current overrides; a one-shot copy, not a live link).
  • ResetToDefaults() — clear all Style + Lighting overrides.
  • SaveAsPreset() — editor only: save the volume's current Style + Lighting as a new UCelshadingPreset asset and link it as ActivePreset.

Volume properties — bVolumeActive, StencilSlot, bUnbound, SpatialFalloff, ContributionWeight, BlendPriority, PatternDepthReference, ActivePreset and the Settings (style) and Lighting override structs — are BlueprintReadWrite; the subsystem re-evaluates volumes every tick, so runtime changes apply on the next frame.

Parameter glossary

A handful of parameters benefit from a glossary entry:

  • HighlightCutoff — lighting magnitude that maps to the brightest band. Lower this to clamp bright highlights; raise to admit a wider HDR range into quantization.
  • HighlightEntryPoint / ShadowEntryPoint — hard cutoffs. Anything above the highlight entry point saturates to HighlightCutoff; anything below the shadow entry point collapses to ShadowFloorBrightness. The middle bands fill the range between.
  • InternalLineThreshold — depth knee (cm) below which depth differences don't draw an outline. Filters texture noise on flat surfaces.
  • OutlineThicknessNear / Far — kernel diameter (in pixels) at near/far depth. Lerps with camera-to-pixel distance to keep silhouettes visually consistent across the scene. UI slider stops at 32, but the cap is soft — type any value freely; the shader's static loop bounds handle up to 32 px and saturate beyond. Cost scales O(N²) so bump only as needed.
  • FogAware — when on, the pass reconstructs height + volumetric fog, removes it before quantization (so colors are clean), then re-applies it at the end. Avoid disabling unless you have a custom post-process fog setup.
  • SpecularHighlightColor — color of the inner highlight rim added by the metallic-compensation path. Default white.

FAQ

Does it work with Lumen, volumetric fog, and volumetric lights?

Yes — all of them feed into the scene-color input the pass quantizes. Lumen GI, exponential height fog, volumetric fog, and volumetric lights (god rays / light shafts from Directional and Point lights) all participate in the cel-shaded output without extra setup.

Does it work with Subsurface / Hair / Cloth shading models?

Yes. The pass gates pixels — it does not modify materials. Any UE shading model is supported.

Mobile / Forward / Vulkan?

v1.0 supports Windows and the deferred renderer (SM5 / SM6). Mobile, the forward renderer and other platforms are not supported yet — see Compatibility.

Does it cost much GPU time?

One compute pass over the view per stencil slot that has a volume (1..255 — pay only for slots you actually populate), a second pass for slots with the outside outline (OutsideOutlineThickness ≥ 1), and two scene-colour copies. Empty slots cost zero — they don't dispatch — and pixels that don't carry a pass's stencil value exit early, so the cost grows with resolution and the number of active slots.

Measure it on your own target hardware: stat gpu shows a Celshading row, and ProfileGPU or Unreal Insights break it down per pass (Celshading.Cel[N], Celshading.Halo[N]). For reference, the ten-slot Showcase_Presets demo map measured 0.42 ms for the whole Celshading scope on an RTX 3090 Ti in an editor viewport of about half a megapixel.

Troubleshooting

Nothing happens when I tick CustomDepth on an actor

  • Confirm Project Settings → Rendering → Custom Depth-Stencil Pass = Enabled with Stencil (the plugin auto-promotes this on startup; the Output Log then shows LogCelshading: UltimateCelshadingPBR: r.CustomDepth was N … promoting to 3 (Enabled with Stencil). If the setting is still lower, another config or device profile forces r.CustomDepth).
  • Confirm r.Celshading.MasterEnable is 1 (default).
  • Confirm the actor's CustomDepth Stencil Value matches the volume's StencilSlot (or r.Celshading.CelStencilRef = 1 for the default no-volume single-dispatch fallback).

Volume Style overrides don't apply

  • Each field has a checkbox next to it — flip the override toggle on for the field to participate.
  • If the volume has bUnbound = false, make sure the camera is inside the box or within SpatialFalloff.

Lighting overrides don't apply

  • The plugin auto-discovers the first ADirectionalLight, ASkyLight, AExponentialHeightFog in the level. Make sure these exist.
  • If you re-keyed the lights manually after PIE, call UCelshadingSubsystem::RefreshBase().

Compatibility

Ultimate Celshading PBR is a native UE5 C++ render plugin, distributed via Fab.

  • ✅ Unreal Engine 5 projects, Blueprint-only or C++ — editor, Play In Editor and packaged Windows builds. The supported engine versions are listed on the Fab page.
  • ✅ Deferred renderer (SM5 / SM6): Lumen, shadow maps, virtual shadow maps, fog and volumetrics.
  • ✅ Every UE shading model — the pass reads the GBuffer and never edits your materials.
  • ❌ Mobile and the forward renderer are not supported in v1.0.
  • ❌ UEFN / Fortnite — UEFN does not support C++ plugins. This plugin will not appear in UEFN's plugin list and cannot be installed there.

Support

Questions, bug reports and feature requests: [email protected], or the Questions tab on the plugin's Fab page.

To get an answer on the first reply, include:

  • Unreal Engine version and plugin version (Edit → Plugins shows both).
  • What you expected and what you see — a screenshot helps.
  • The Output Log lines that start with LogCelshading, and the output of DumpCVars r.Celshading. if you changed any console variables.