Shader Design Patterns
Now, it’s time to design a custom pattern to illustrate the shader. Since all vertex and fragment is “blind” to others, we have to script a code with different manner from the concurrent programming. Thus, the position and color of a vertex should be defined with its own attributes and the shared uniform values. Here, I’ll address several techniques. For more information, please visit https://thebookofshaders.com
Basic Functions
step(th, x)
: return \(1\) if \(\rm{th} \it < x\), otherwise \(0\).smoothstep(th1, th2, x)
: return \(0\) if \(x < \rm{th}_1\), \(1\) if \(\rm{th}_2 \it < x\), otherwise smoothed interpolated value between [0, 1]- mathematical operation
abs(x)
: return absolute value, \(\|x\|\)sin, cos, tan, ...
: trigonometric functionsmin(x, y)
: return the smaller valuemax(x, y)
: return the larger valuepow(x, y)
: return the value of \(x\) to the power of \(y\); \(~x^y\)dot(v, w)
: return the dot product of vectors \(v\) and \(w\); \(~v \cdot w\)cross(v, w)
: return the cross product of vectors \(v\) and \(w\); \(~v \times w\)mod(x, y)
: return the remainder of a division \(x/y\); \(~x - y * \rm floor(\it x/y \rm)\)fract(x)
: return the fractional part of \(x\); \(~x - \rm floor(\it x \rm)\)
mix(x, y, r)
: return the interpolated value between \(x\) and \(y\) with a weight \(r\)clamp(x, th1, th2)
: return the truncated value of \(x\) between \(\rm{th}_1\) and \(\rm{th}_2\), i.e., \(\rm min(max( \it x, \rm{th}_1), \rm{th}_2)\)length(v)
: return the Euclidean length of vector \(v\); \(~\|v\|\)distance(v, w)
: return the Euclidean length of vector \(v-w\); \(~\|v-w\|\)
Using the above fundamental functions, several techniques are addressed below.
Gradient
Gradient color shows a transition between multiple colors. To implement gradient color, we need \(n\) colors and \((n-1)\) weight variables.
mix
makes you implement simple gradient easily.
varying vec2 v_position; // [0, 1]
void main() {
vec3 color1 = vec3(1.0, 0.5, 0.5);
vec3 color2 = vec3(0.5, 1.0, 1.0);
gl_FragColor = vec4(mix(color1, color2, v_position.x), 1.0);
}
Or, you can use smoothstep
so that the position of gradient boundary can be tuned.
varying vec2 v_position; // [0, 1]
void main() {
vec3 color1 = vec3(1.0, 0.5, 0.5);
vec3 color2 = vec3(0.5, 1.0, 1.0);
float ratio = smoothstep(0.3, 0.7, v_position.x);
gl_FragColor = vec4(mix(color1, color2, ratio), 1.0);
}
Also, you can manually design the smoothing function.
varying vec2 v_position; // [0, 1]
float my_interp(in float a, in float b, in float x) {
float z = (x-a)/(b-a);
return z < 0.5 ? 2.0 * z * z : 1.0 - pow(-2.0 * z + 2.0, 2.0) / 2.0;
}
void main() {
vec3 color1 = vec3(1.0, 0.5, 0.5);
vec3 color2 = vec3(0.5, 1.0, 1.0);
float ratio = my_interp(0.0, 1.0, v_position.x);
gl_FragColor = vec4(mix(color1, color2, ratio), 1.0);
}
If you want to generate a gradient using three colors, use mix
twice with two weight variables.
varying vec2 v_position; // [0, 1]
void main() {
vec3 color1 = vec3(1.0, 0.5, 0.5);
vec3 color2 = vec3(0.5, 1.0, 1.0);
vec3 color3 = vec3(0.2, 0.5, 1.0);
float ratio1 = smoothstep(0.0, 0.6, v_position.x);
float ratio2 = smoothstep(0.4, 1.0, v_position.x);
gl_FragColor = vec4(mix(mix(color1, color2, ratio1), color3, ratio2), 1.0);
}
Repetitive Pattern
When zooming out on most complicated natural texture, you can see they are patterned. To divide a range [0, 1] into multiple [0, 1] s, we use fract()
function.
varying vec2 v_position; // [0, 1]
void main() {
float ncol = 3.0;
float nrow = 2.0;
float x1 = fract(ncol * v_position.x);
float y1 = fract(nrow * v_position.y);
float idx_x1 = floor(ncol * v_position.x);
float idx_y1 = floor(nrow * v_position.y);
gl_FragColor = vec4(x1, y1, (idx_x1 + idx_y1) / 5.0, 1.0);
}
Likewise, if you want to generate a pattern inside a pattern, divide grids twice.
varying vec2 v_position; // [0, 1]
vec4 divide_cell(in vec2 parent, in vec2 size) {
vec4 child = vec4(0.0);
child.xy = fract(size * parent); // x, y position
child.zw = floor(size * parent); // x, y index
return child;
}
void main() {
vec4 xy1 = divide_cell(v_position, vec2(3.0, 2.0));
vec4 xy2 = divide_cell(xy1.xy, vec2(2.0, 2.0));
gl_FragColor = vec4(xy2.x, xy2.y, mod(xy2.z + xy2.w, 2.0), 1.0);
}
Random
Unfortunately, GLSL does not provide a true random number generator. Instead, we can generate a pseudo random number with a sinusoidal function of high amplitude with fract
. fract
transforms the value into the range [0, 1], and the sinusoidal function produces randomness by generating a different slope for each [0, 1] piece. Also, fract(x^2)
can generate random number since their slopes are different for each piece. However, unlike the sinusoidal function, which is bounded on [-1, 1], x^2
can exceed the maximum value of Float, thus outputting incorrect value.
float random(in float x) {
return fract(sin(x)*100000.0);
}
Using MATLAB, I’ve generated the histogram of the above random variable.
x = linspace(0, pi, 10000);
figure();
set(gcf, 'Position', [680 557 720 480])
z = sin(x)*100000;
%% z = x.^2*100000;
%% z = x*100000;
X = z - floor(z);
subplot(211);
histogram(X, 'Normalization', 'pdf');
xlim([0, 1]);
grid on
xlabel('Value, X');
ylabel('PDF, f(X)');
set(gcf, 'color', 'w');
subplot(212);
plot(x, X, '.');
xlim([0, pi]);
ylabel('Value, X');
formula | result |
---|---|
\(\rm sin(\it x)\) | |
\(x^2\) | |
\(x\) |
Above, the histogram of \(\rm sin(\it x \rm)\), \(x^2\), \(x\) are shown. As mentioned before, sinusoidal and power functions show randomness, whereas linear function has a pattern. Please notice that they have a uniform distribution.
To generate a random number from two or more dimensional vector, we use dot product to produce a scalar number.
float random(in vec2 x) {
return fract(sin(dot(x, vec2(12.9898,78.233)))*43758.5453123);
}
The above magic numbers can be chosen manually so that randomness is shown well.
Noise
If we called a single independent noise as random, noise means the interpolated value between random values. Thus, noise functions have a continuity, whereas random functions show discontinuity. Below are the example of random and noise.
random | noise |
---|---|
As mentioned above, all vertex and fragment is blind to others, so each fragment can not read the colors nearby. However, since the random function of GLSL generates pseudo-random output rather than true random, thus each fragment can read the value of adjacent fragments.
float random (in vec2 x) {
return fract(sin(dot(x, vec2(12.9898,78.233))) * 43758.5453123);
}
float noise (in vec2 x) {
vec2 i = floor(x);
vec2 f = fract(x);
// Four corners in 2D of a tile
float a = random(i);
float b = random(i + vec2(1.0, 0.0));
float c = random(i + vec2(0.0, 1.0));
float d = random(i + vec2(1.0, 1.0));
// Cubic Hermine Curve. Same as SmoothStep()
vec2 u = smoothstep(0., 1., f);
// Mix 4 coorners percentages
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
In real world, a signal is the sum of sinusoids of multiple frequencies, and we can compute the amplitude of sinusoids of the specific frequency by Fourier transform. In noise pattern, a division number corresponds to a frequency of Fourier transform. Therefore, when we add multiple noises of different division size, a natural texture can be imitated.
float random (in vec2 x) {
return fract(sin(dot(x, vec2(12.9898,54.233))) * 43758.5453123);
}
float noise (in vec2 x) {
vec2 i = floor(x);
vec2 f = fract(x);
float tl = random(i); // top-left corner
float tr = random(i + vec2(1.0, 0.0)); // top-right corner
float bl = random(i + vec2(0.0, 1.0)); // bottom-left corner
float br = random(i + vec2(1.0, 1.0)); // bottom-right corner
vec2 u = smoothstep(0., 1., f);
// Mix 4 coorners percentages
return mix(mix(tl, tr, u.x), mix(bl, br, u.x), u.y);
}
#define LEVELS 6
float fractal_noise (in vec2 x, in float scaling_amp, in float scaling_freq) {
float n = 0.;
float amplitude = 1. - scaling_amp; // ensure that the maximum value is 1.
for (int i = 0; i < LEVELS; ++i) {
n += amplitude * noise(x);
amplitude *= scaling_amp;
x *= scaling_freq;
}
return n;
}
void main() {
gl_FragColor = vec4(vec3(fractal_noise(2.*v_position, 0.5, 2.)), 1.);
}
Also, you can compose multiple fractal noises and time variable to illustrate flowing textures such as smoke.
void main() {
vec3 color = vec3(0.5, 0.6, 0.7);
float n = fractal_noise(2.*v_position, 0.5, 2.);
float m = fractal_noise(2.*v_position + vec2(0.1, 0.12)*u_time + n, 0.5, 2.);
gl_FragColor = vec4(mix(vec3(0.), color, m), 1.);
}