OpenGL ES Pixel Shaders Tutorial
In this OpenGL ES pixel shaders tutorial, take a deep dive into GLSL and fragment shader math – including how to make gradients and random noise! By Ricardo Rendon Cepeda.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
OpenGL ES Pixel Shaders Tutorial
50 mins
- Getting Started
- Pixel Shaders vs Vertex/Fragment Shaders
- Pixel Shaders 101: Gradients
- Pixel Shader Geometry
- Geometry: 2D Disc
- Geometry: 3D Sphere
- Pixel Shader Procedural Textures: Perlin Noise
- Procedural Textures: Time
- Procedural Textures: "Random" Noise
- Procedural Textures: Square Grid
- Procedural Textures: Smooth Noise
- Procedural Textures: Interpolated Noise
- Procedural Textures: Moving Noise
- Pixel Shader Moon
- Where To Go From Here?
Procedural Textures: Interpolated Noise
The next step for your noise shader is rid the tiles of hard edges by using bilinear interpolation, which is simply linear interpolation on a 2D grid.
For ease of comprehension, the image below shows the desired sampling points for bilinear interpolation within your noise function roughly translated to your previous 2x2 grid:
Tiles can blend into one another by sampling weighted values from their corners at point P
. Since each tile is 1x1 unit, the Q
points should be sampling noise like so:
Q11 = smoothNoise(0.0, 0.0);
Q12 = smoothNoise(0.0, 1.0);
Q21 = smoothNoise(1.0, 0.0);
Q22 = smoothNoise(1.0, 1.0);
In code, you achieve this with a simple combination of floor()
and ceil()
functions for p
. Add the following function to RWTNoise.fsh
, just above main(void)
:
float interpolatedNoise(vec2 p) {
float q11 = smoothNoise(vec2(floor(p.x), floor(p.y)));
float q12 = smoothNoise(vec2(floor(p.x), ceil(p.y)));
float q21 = smoothNoise(vec2(ceil(p.x), floor(p.y)));
float q22 = smoothNoise(vec2(ceil(p.x), ceil(p.y)));
// compute R value
// return P value
}
GLSL already includes a linear interpolation function called mix()
.
You'll use it to compute R1
and R2
, using fract(p.x)
as the weight between two Q
points at the same height on the y-axis. Include this in your code by adding the following lines at the bottom of interpolatedNoise(vec2 p)
:
float r1 = mix(q11, q21, fract(p.x));
float r2 = mix(q12, q22, fract(p.x));
Finally, interpolate between the two R
values by using mix()
with fract(p.y)
as the floating-point weight. Your function should look like the following:
float interpolatedNoise(vec2 p) {
float q11 = smoothNoise(vec2(floor(p.x), floor(p.y)));
float q12 = smoothNoise(vec2(floor(p.x), ceil(p.y)));
float q21 = smoothNoise(vec2(ceil(p.x), floor(p.y)));
float q22 = smoothNoise(vec2(ceil(p.x), ceil(p.y)));
float r1 = mix(q11, q21, fract(p.x));
float r2 = mix(q12, q22, fract(p.x));
return mix (r1, r2, fract(p.y));
}
Since your new function requires smooth, floating-point weights and implements floor()
and ceil()
for sampling, you must remove floor()
from main(void)
.
Replace the lines:
float tiles = 8.;
position = floor(position*tiles);
float n = smoothNoise(position);
With the following:
float tiles = 8.;
position *= tiles;
float n = interpolatedNoise(position);
Build and run. Those hard tiles are gone…
… but there is still a discernible pattern of "stars", which is totally expected, by the way.
You’ll get rid of the undesirable pattern with a smoothstep function. smoothstep()
is a nicely curved function that uses cubic interpolation, and it's much nicer than simple linear interpolation.
Add the following line inside interpolatedNoise(vec2 p)
, at the very beginning:
vec2 s = smoothstep(0., 1., fract(p));
Now you can use s
as the smooth-stepped weight for your mix()
functions, like so:
float r1 = mix(q11, q21, s.x);
float r2 = mix(q12, q22, s.x);
return mix (r1, r2, s.y);
Build and run to make those stars disappear!
The stars are definitely gone, but there’s still a bit of a pattern; almost like a labyrinth. This is simply due to the 8x8 dimensions of your square grid. Reduce tiles
to 4.
, then build and run again!
Much better.
Your noise function is still a bit rough around the edges, but it could serve as a texture primitive for billowy smoke or blurred shadows.
Procedural Textures: Moving Noise
Final stretch! Hope you didn’t forget about little ol' uTime
, because it’s time to animate your noise. Simply add the following line inside main(void)
, just before assigning n
:
position += uTime;
Build and run.
Your noisy texture will appear to be moving towards the bottom-left corner, but what’s really happening is that you’re moving your square grid towards the top-right corner (in the +x, +y direction). Remember that 2D noise extends infinitely in all directions, meaning your animation will be seamless at all times.
Pixel Shader Moon
Hypothesis: Sphere + Noise = Moon? You're about to find out!
To wrap up this tutorial, you’ll combine your sphere shader and noise shader into a single moon shader in RWTMoon.fsh. You have all the information you need to do this, so this is a great time for a challenge!
Hint: Your noise tiles
will now be defined by the sphere’s radius
, so replace the following lines:
float tiles = 4.;
position *= tiles;
With a simple:
position /= radius;
Also, I double-dare you to refactor a little bit by completing this function:
float diffuseSphere(vec2 p, float r) {
}
[spoiler title="Werewolves, Beware"]
// RWTMoon.fsh
//
// Precision
precision highp float;
// Uniforms
uniform vec2 uResolution;
uniform float uTime;
// Constants
const vec3 cLight = normalize(vec3(.5, .5, 1.));
float randomNoise(vec2 p) {
return fract(6791.*sin(47.*p.x+p.y*9973.));
}
float smoothNoise(vec2 p) {
vec2 nn = vec2(p.x, p.y+1.);
vec2 ee = vec2(p.x+1., p.y);
vec2 ss = vec2(p.x, p.y-1.);
vec2 ww = vec2(p.x-1., p.y);
vec2 cc = vec2(p.x, p.y);
float sum = 0.;
sum += randomNoise(nn)/8.;
sum += randomNoise(ee)/8.;
sum += randomNoise(ss)/8.;
sum += randomNoise(ww)/8.;
sum += randomNoise(cc)/2.;
return sum;
}
float interpolatedNoise(vec2 p) {
vec2 s = smoothstep(0., 1., fract(p));
float q11 = smoothNoise(vec2(floor(p.x), floor(p.y)));
float q12 = smoothNoise(vec2(floor(p.x), ceil(p.y)));
float q21 = smoothNoise(vec2(ceil(p.x), floor(p.y)));
float q22 = smoothNoise(vec2(ceil(p.x), ceil(p.y)));
float r1 = mix(q11, q21, s.x);
float r2 = mix(q12, q22, s.x);
return mix (r1, r2, s.y);
}
float diffuseSphere(vec2 p, float r) {
float z = sqrt(r*r - p.x*p.x - p.y*p.y);
vec3 normal = normalize(vec3(p.x, p.y, z));
float diffuse = max(0., dot(normal, cLight));
return diffuse;
}
void main(void) {
vec2 center = vec2(uResolution.x/2., uResolution.y/2.);
float radius = uResolution.x/2.;
vec2 position = gl_FragCoord.xy - center;
if (length(position) > radius) {
discard;
}
// Diffuse
float diffuse = diffuseSphere(position, radius);
// Noise
position /= radius;
position += uTime;
float noise = interpolatedNoise(position);
gl_FragColor = vec4(vec3(diffuse*noise), 1.);
}
[/spoiler]
Remember to change your program’s fragment shader source to RWTMoon
in RWTViewController.m
:
self.shader = [[RWTBaseShader alloc] initWithVertexShader:@"RWTBase" fragmentShader:@"RWTMoon"];
While you’re there, feel free to change your glClearColor()
to complement the scene a bit more (I chose xkcd’s midnight purple):
glClearColor(.16f, 0.f, .22f, 1.f);
Build and run! Oh yeah, I’m sure Ozzy Osbourne would approve.