As of lately I've been studying a bit of shaders (in Unity), and am trying to recreate the "light-based inline" that can be observed in the latest Legend of Zelda. But maybe the task is a bit above my knowledge, for now.

As from what I can analyze, it seems to behave (when moving) similar to what a "rim shading" does, except it has a sharp cutoff (simple to do), and — the part I'm struggling with — it seems to have a constant width independent from the local curvature of the mesh and distance from camera, essentially having a constant "width" in relation to the screen. (well it actually varies a bit with camera distance, but mostly it doesn't so let's assume it doesn't)

It's a bit hard to show in a still, but you can find plenty videos on the game, and the effect is visible everywhere.

I've managed to do the simpler part, the cut-off rim shading. But this gives the awkward effect that depending on camera orientation and the geometry curvature, the "inline" occupies more or less screen area. Below is an image of the current results I get on various camera angles, with green arrows showing good/acceptable areas and red arrows demonstrating deviation from the intended effect.

This is my current implementation for reference, as a Unity Surface Light Model:

half4 LightingToonPoster(SurfaceOutputStandard surf, float3 viewDir, UnityGI gi) {
// Material properties
half3 specular;
half oneMinusReflectivity;
DiffuseAndSpecularFromMetallic(surf.Albedo, surf.Metallic, /*out*/ specular, /*out*/ oneMinusReflectivity);
float3 reflected = normalize(-reflect(gi.light.dir, surf.Normal));
float specularity = dot(viewDir, reflected) * _Specularity;

// Light setup
float ndotl = dot(surf.Normal, gi.light.dir) * 0.5 + 0.5;
float lightIncidence = gi.light.color * ndotl;
float3 ramp = tex2D(_Ramp, lightIncidence.xx).rgb;
float3 specularRamp = tex2D(_Ramp, specularity.xx).rgb;
gi.light.color = ramp * _LightColor0.rgb;

// Rim and camera-light convergence
float rim = 1 - saturate(dot(surf.Normal, viewDir));
float facing = -dot(viewDir, gi.light.dir) * 0.5 + 0.25;

// sigmoid of rim for smooth falloff centered at _EdgeBias
float outline = 1 / (1 + exp(-100 * (rim - _EdgeBias)));
float highlight = _HighlightStrength * outline * facing * outline;

float4 std = LightingStandard(surf, viewDir, gi);

float3 toonDiffuse = surf.Albedo * (gi.light.color / 2 + ShadeSH9(float4(viewDir, 1)));
float3 toonSpecular = specular * gi.indirect.specular * specularRamp;
float4 toon = half4(toonDiffuse + toonSpecular, surf.Alpha);
return lerp(std, toon, _UnshadeFactor) * half4(1 + highlight.xxx, 1);
}


Or check this Gist for the full shader file

The question now being, what can I use instead of rim-shading to obtain a similar effect, where the curvature is not taken into account and I get a constant "inset"?

• Frankly, I like the look you've achieved better! Jul 14, 2017 at 1:16
• @user1118321 I would be satisfied too, if not for the fact that it silhouettes perfectly every minor problem with the models it is applied to. Jul 14, 2017 at 1:21

Nice question! I hadn't seen the game myself but this question got me interested so I watched some footage on Youtube.

This one for example gives a good view of it: https://youtu.be/Ktjdg3zgRzk?t=15m46s

A few things I notice:

• Hair and right leg: note how only the diffuse lit part has rim lighting.
• Left wrist: note how the width becomes thinner at the angle with the hand.
• On some other part you can also see the rim lighting is affected by cast shadows too.

From these observations, I think there are two effects used:

1. A diffuse shading with a sharp transition between lit a unlit, that happens somewhere below 0 so the light leaks behind a little.
2. The rim lighting that, as you mention, has an almost constant screenspace width.

From the look of it and some of the artifacts, my best guess is that it's actually some edge detection filter based on depth, which is then used in combination with the diffuse lighting so the rim lighting doesn't affect the unlit parts.

It's a pretty cool effect, thank you for point it out!

• I thought of edge detection, but when observing the same effect ingame in the terrain (which has rather large triangles), it is possible to see the same transition between a "wide" rim and the constant-width as the camera turns to a narrower angle. Also, what kind of edge detection could be used here? I actually tried using Sobel on depth, but then I have no idea how to adjust the "width" without blurring internal lines. (Though I imagine this should be a separate question) Jul 13, 2017 at 12:31
• They probably distinguish characters and terrain (I thought the effect was absent on terrain, but maybe I just missed it). For Sobel, just scale the kernel to the thickness you want: instead of comparing the.adjacent pixels, compare the ones a bit further. Jul 13, 2017 at 12:58
• Hmm I'll take another dab at Sobel, and report back Jul 13, 2017 at 23:07