Linear sky
A vertical blend from a warm horizon to a deep zenith, with a start and an end you can move.
- 01GoalColor
- 02TechniqueGradients
- 03ExecutionLinear sky
Parameters
Copy for your platform
- GLSLCopy
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
i.uv.yUV + VectorDecomposei.uv.yi.uv.yi.uv.y UV01 Height on the quad
(x - a) / (b - a)Remap(x - a) / (b - a)(x - a) / (b - a)(x - a) / (b - a) In02 Remap to the band
clampClampclampclampclamp In03 Clamp to 0–1
mixMixmixmixmix In04 Mix the two colors
How it works
A gradient needs a number that runs from 0 to 1 and two colors to blend by it. Here the number comes from the height on the quad, and the colors are a warm horizon and a deep blue overhead.
- Height.
i.uv.yis the UVA 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.FundamentalUV coordinates
A 2D position on a surface or the screen, usually from 0 to 1 on both axes. Shaders draw shapes and patterns as functions of it.
uv = gl_FragCoord.xy / resolutionSources’s vertical coordinate: 0 at the bottom edge, 1 at the top. Shown as a color it is already a gradient, from black to white.- gl_FragCoord (opens in a new tab)Reference
- UV mapping (opens in a new tab)Reference
- Remap. A real sky doesn’t change color evenly from the ground to the top of the frame: it stays pale near the
horizon and deepens higher up.
(height - p_bottom) / (p_top - p_bottom)moves the blend into a band: 0 at the start height, 1 at the end height. This is the inverse of a mix: it asks how far along the band the pixel is. - Clamp. Below the start the remap goes negative, and above the end it passes 1.
clampholds it at the ends, so the horizon color fills everything below the band and the zenith color everything above it. Without it, the mixBlends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.FundamentalLinear interpolation (mix)
Blends from a to b by a factor t, along a straight line. GLSL calls it mix; HLSL and most node editors call it lerp.
mix(a, b, t) = a (1 − t) + b tSourcesin the next step would extrapolate past both colors.- mix (opens in a new tab)Reference
- Linear interpolation (opens in a new tab)Reference
- Mix.
mix(p_horizon, p_zenith, t)blends the two colors channel by channel: each of red, green and blue moves in a straight line from one end to the other.
The colors arrive in linear light, because the frame decodes color params before the shader sees them and encodes the
result once at the end. So the mix blends light the way overlapping light blends, and the middle stays bright. Mixing
the sRGBThe standard encoding of screen colors. Stored values follow a curve close to a 1/2.2 power of the light, so they must be decoded before lighting or blending. sRGB The standard encoding of screen colors. Stored values follow a curve close to a 1/2.2 power of the light, so they must be decoded before lighting or blending.linear = ((c + 0.055) / 1.055)^2.4 for c > 0.04045
Move the start and end close together and the band turns into a hard horizon line; spread them to the edges and the
gradient takes the whole height. An eight-bit screen has only 256 levels per channel, so a long, slow gradient between
similar colors can show faint steps (bandingBanding is the stripes a smooth gradient breaks into when stored with too few levels. Dithering adds tiny per-pixel noise before rounding, so the stripes become fine grain. Banding & dithering Banding is the stripes a smooth gradient breaks into when stored with too few levels. Dithering adds tiny per-pixel noise before rounding, so the stripes become fine grain.
Make it in GLSL
The copied code is a complete GLSL ES 3.00 fragment shader for WebGL2. It reads v_uv from the vertex shader, so draw
it on any quad or full-screen triangle whose vertex shader passes UVs from 0 at the bottom-left to 1 at the top-right.
It also declares v_position, v_normal and v_view; a 2D vertex shader can write constants into them.
- Compile it with a vertex shader that outputs
v_uv(and the other three varyings) and link the two. - Upload the four uniforms:
p_horizonandp_zenithasvec3in linear light,p_bottomandp_topas floats between 0 and 1. Decode a picker’s sRGB value per channel withc <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4. - Draw. The shader’s
main()clamps the result and encodes it to sRGB itself, because a WebGL2 canvas doesn’t.
To use the sky inside a bigger shader, take only effect() and call it with i.uv set; then encode once at the very
end of your own shader, not inside the sky.
Make it in Godot
The shader assumes HDR 2D is off (the project default), so it decodes the color uniforms and encodes the result itself. With Rendering > Viewport > HDR 2D on, make u_toLinear() and u_toSrgb() return c unchanged.
The export is a canvas_item shader, so it draws on any 2D node.
- Add a ColorRect and set its layout to Full Rect (or the size of the panel you want behind your UI).
- In Material, create a ShaderMaterial, then a new Shader, and paste the code over the template.
- The four uniforms show under Shader Parameters: pick the horizon and zenith colors, and move the start and end between 0 and 1.
In VisualShader the same chain is UV → VectorDecompose (y) → Remap (input range from start to end, output 0 to 1) →
Clamp → Mix between two ColorParameter nodes. For a sky that turns with a 3D camera, write a sky shader that mixes by
EYEDIR.y instead: the same remap and mix, a different number for t.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Gradients → Linear sky, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform vec3 p_horizon; // Horizon color: vec3(0.99, 0.72, 0.5), sRGB; upload it decoded to linearuniform vec3 p_zenith; // Zenith color: vec3(0.16, 0.3, 0.68), sRGB; upload it decoded to linearuniform float p_bottom; // Gradient start: 0.1 (0.0 to 0.45)uniform float p_top; // Gradient end: 0.9 (0.55 to 1.0)in vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// The colors arrive linear, so this mix blends light; mixing their sRGB values instead darkens the middle// (the RGB vs OKLab ramp card shows the difference).vec4 effect(Inputs i) { float height = i.uv.y; float ramp = (height - p_bottom) / (p_top - p_bottom); float t = clamp(ramp, 0.0, 1.0); vec3 sky = mix(p_horizon, p_zenith, t); return vec4(sky, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Gradients → Linear sky, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform vec3 horizon : source_color = vec3(0.99, 0.72, 0.5);uniform vec3 zenith : source_color = vec3(0.16, 0.3, 0.68);uniform float bottom : hint_range(0.0, 0.45, 0.01) = 0.1;uniform float top : hint_range(0.55, 1.0, 0.01) = 0.9;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// The colors arrive linear, so this mix blends light; mixing their sRGB values instead darkens the middle// (the RGB vs OKLab ramp card shows the difference).vec4 effect(Inputs i) { float height = i.uv.y; float ramp = (height - bottom) / (top - bottom); float t = clamp(ramp, 0.0, 1.0); vec3 sky = mix(u_toLinear(horizon), u_toLinear(zenith), t); return vec4(sky, 1.0);}void fragment() { Inputs i; // UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = 1.0 / SCREEN_PIXEL_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.