Shader concept
Fresnel effect in shaders: the rim glow
The Fresnel effect is how surfaces look brighter and more reflective at glancing angles. In a shader it is usually written as pow(1 - dot(normal, viewDir), k) and used for rim glow, water and glass.
Running live in your browser. The code below is exactly what draws it.
What is going on physically
Look straight down at a pond and you see through the water; look across it at a shallow angle and it turns into a mirror. Every smooth surface behaves this way: the more glancing the angle between the view and the surface, the more light is reflected. That change is the Fresnel effect.
The shader version
You need two directions: the normal (which way the surface faces) and the view direction (from the surface to the eye). Their dot product is 1 when you look straight on and 0 at the silhouette. Turn it round with 1 - dot(n, v) and raise it to a power: the larger the power, the thinner and sharper the rim. Multiply a colour (a sky reflection, a glow) by the result and add it to the surface.
Where it is used
- Rim light on characters and props, so they stand out from the background.
- Water: mix between "see the depths" and "reflect the sky" with the Fresnel term. See the ocean lessons.
- Glass, force fields and holograms: glowing edges with a transparent middle.
Common mistakes
- Using an un-normalised normal or view vector, which makes the dot product exceed 1 and the rim vanish. Normalise both.
- Forgetting the view direction must point toward the camera; pointing it away flips the effect.
The code, in every shader language
The same example for each language family. Open one to see it.
HLSL Unity, Unreal
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0) * 2.4;
float r2 = dot(p, p);
float3 bg = float3(0.04, 0.05, 0.12);
if (r2 > 1.0)
{
return float4(bg, 1.0);
}
float3 n = float3(p.x, p.y, sqrt(1.0 - r2)); // the surface direction of a sphere facing us
float3 view = float3(0.0, 0.0, 1.0); // looking straight at it
float facing = saturate(dot(n, view)); // 1 in the middle, 0 at the edge
float fres = pow(1.0 - facing, 3.0); // the Fresnel term
float3 base = float3(0.08, 0.12, 0.25) * (0.4 + 0.6 * facing);
float3 rim = float3(0.35, 0.9, 1.0) * fres;
return float4(base + rim, 1.0);
} GLSL Shadertoy, three.js, OpenGL
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 2.4;
float r2 = dot(p, p);
vec3 bg = vec3(0.04, 0.05, 0.12);
if (r2 > 1.0)
{
return vec4(bg, 1.0);
}
vec3 n = vec3(p.x, p.y, sqrt(1.0 - r2)); // the surface direction of a sphere facing us
vec3 view = vec3(0.0, 0.0, 1.0); // looking straight at it
float facing = clamp(dot(n, view), 0.0, 1.0); // 1 in the middle, 0 at the edge
float fres = pow(1.0 - facing, 3.0); // the Fresnel term
vec3 base = vec3(0.08, 0.12, 0.25) * (0.4 + 0.6 * facing);
vec3 rim = vec3(0.35, 0.9, 1.0) * fres;
return vec4(base + rim, 1.0);
} WGSL WebGPU, Bevy
fn frag(uv: vec2f) -> vec4f
{
let p = (uv - 0.5) * vec2f(aspect, 1.0) * 2.4;
let r2 = dot(p, p);
let bg = vec3f(0.04, 0.05, 0.12);
if (r2 > 1.0)
{
return vec4f(bg, 1.0);
}
let n = vec3f(p.x, p.y, sqrt(1.0 - r2)); // the surface direction of a sphere facing us
let view = vec3f(0.0, 0.0, 1.0); // looking straight at it
let facing = saturate(dot(n, view)); // 1 in the middle, 0 at the edge
let fres = pow(1.0 - facing, 3.0); // the Fresnel term
let base = vec3f(0.08, 0.12, 0.25) * (0.4 + 0.6 * facing);
let rim = vec3f(0.35, 0.9, 1.0) * fres;
return vec4f(base + rim, 1.0);
} Godot Godot 4 shading language
shader_type canvas_item;
vec4 frag(vec2 uv)
{
vec2 p = (uv - 0.5) * vec2(aspect, 1.0) * 2.4;
float r2 = dot(p, p);
vec3 bg = vec3(0.04, 0.05, 0.12);
if (r2 > 1.0)
{
return vec4(bg, 1.0);
}
vec3 n = vec3(p.x, p.y, sqrt(1.0 - r2)); // the surface direction of a sphere facing us
vec3 view = vec3(0.0, 0.0, 1.0); // looking straight at it
float facing = clamp(dot(n, view), 0.0, 1.0); // 1 in the middle, 0 at the edge
float fres = pow(1.0 - facing, 3.0); // the Fresnel term
vec3 base = vec3(0.08, 0.12, 0.25) * (0.4 + 0.6 * facing);
vec3 rim = vec3(0.35, 0.9, 1.0) * fres;
return vec4(base + rim, 1.0);
}
void fragment()
{
// Godot's UV starts at the top-left, the lessons use bottom-left
COLOR = frag(vec2(UV.x, 1.0 - UV.y));
} Metal iOS, macOS
float4 frag(float2 uv)
{
float2 p = (uv - 0.5) * float2(aspect, 1.0) * 2.4;
float r2 = dot(p, p);
float3 bg = float3(0.04, 0.05, 0.12);
if (r2 > 1.0)
{
return float4(bg, 1.0);
}
float3 n = float3(p.x, p.y, sqrt(1.0 - r2)); // the surface direction of a sphere facing us
float3 view = float3(0.0, 0.0, 1.0); // looking straight at it
float facing = saturate(dot(n, view)); // 1 in the middle, 0 at the edge
float fres = pow(1.0 - facing, 3.0); // the Fresnel term
float3 base = float3(0.08, 0.12, 0.25) * (0.4 + 0.6 * facing);
float3 rim = float3(0.35, 0.9, 1.0) * fres;
return float4(base + rim, 1.0);
} Practise it in the lessons
- Fake 3D: normals & lightShade the disc as a sphere: compute z and the normal, then diffuse light from the given direction.
- Fresnel rim glowA dark blue knot with a cyan glow around the edges.
- Ocean 3: sky, Fresnel and sun glitterPlusGive the sea a real sky with a sun, mix the water and the reflected sky with a Fresnel factor, and add the…
Questions
What is Schlick's approximation?
A cheap formula for the Fresnel term: F0 + (1 - F0) * (1 - dot(n, v))^5, where F0 is the reflectance when looking straight on. It matches the real curve closely and is what most game shaders use.
Why is the edge of my sphere not glowing?
Check that the normal and view vector are normalised and that the view vector points toward the camera. Raise the power if the rim is too wide.