For a client I needed a way to draw a gradient in realtime generated from four points, on a mobile phone. To produce an image like this, where the four gradient points can be moved in realtime:
I wanted to implement this using a shader, but after googling for a while I couldn’t find an existing implementation.
The problem is that I have the desired colours at four points, and need to calculate the colour at any other arbitrary point.
So we have three simultaneous equations (vectors in bold):
The key to solving this is to reduce the number of freedoms by setting .
Solving for gives us:
And solving for and
:
so:
Defining:
Lets us now rewrite as:
Which written out for x and y is:
Now we have to consider the various different cases. If then we have:
Likewise if then we have:
Otherwise we are safe to divide by and so:
Which can be solved with the quadratic formula when A is not zero: (Note that only the positive solution makes physical sense for us, and we can assume )
and when A is zero (or, for computational rounding purposes, close to zero):
Math Summary
We can determine and
with:
Implementation as a shader:
This can be implemented as a shader as following: (The following can be run directly on https://www.shadertoy.com for example)
void mainImage( out vec4 fragColor, in vec2 fragCoord ) { //Example colors. In practise these would be passed in // as parameters vec4 color0 = vec4(0.918, 0.824, 0.573, 1.0); // EAD292 vec4 color1 = vec4(0.494, 0.694, 0.659, 1.0); // 7EB1A8 vec4 color2 = vec4(0.992, 0.671, 0.537, 1.0); // FDAB89 vec4 color3 = vec4(0.859, 0.047, 0.212, 1.0); // DB0C36 vec2 uv = fragCoord.xy / iResolution.xy; //Example coordinates. In practise these would be passed in // as parameters vec2 P0 = vec2(0.31,0.3); vec2 P1 = vec2(0.7,0.32); vec2 P2 = vec2(0.28,0.71); vec2 P3 = vec2(0.72,0.75); vec2 Q = P0 - P2; vec2 R = P1 - P0; vec2 S = R + P2 - P3; vec2 T = P0 - uv; float u; float t; if(Q.x == 0.0 &amp;&amp; S.x == 0.0) { u = -T.x/R.x; t = (T.y + u*R.y) / (Q.y + u*S.y); } else if(Q.y == 0.0 &amp;&amp; S.y == 0.0) { u = -T.y/R.y; t = (T.x + u*R.x) / (Q.x + u*S.x); } else { float A = S.x * R.y - R.x * S.y; float B = S.x * T.y - T.x * S.y + Q.x*R.y - R.x*Q.y; float C = Q.x * T.y - T.x * Q.y; // Solve Au^2 + Bu + C = 0 if(abs(A) < 0.0001) u = -C/B; else u = (-B+sqrt(B*B-4.0*A*C))/(2.0*A); t = (T.y + u*R.y) / (Q.y + u*S.y); } u = clamp(u,0.0,1.0); t = clamp(t,0.0,1.0); // These two lines smooth out t and u to avoid visual 'lines' at the boundaries. They can be removed to improve performance at the cost of graphics quality. t = smoothstep(0.0, 1.0, t); u = smoothstep(0.0, 1.0, u); vec4 colorA = mix(color0,color1,u); vec4 colorB = mix(color2,color3,u); fragColor = mix(colorA, colorB, t); }
And the same code again, but formatted as a more typical shader and with parameters:
uniform lowp vec4 color0; uniform lowp vec4 color1; uniform lowp vec4 color2; uniform lowp vec4 color3; uniform lowp vec2 p0; uniform lowp vec2 p1; uniform lowp vec2 p2; uniform lowp vec2 p3; varying lowp vec2 coord; void main() { lowp vec2 Q = p0 - p2; lowp vec2 R = p1 - p0; lowp vec2 S = R + p2 - p3; lowp vec2 T = p0 - coord; lowp float u; lowp float t; if(Q.x == 0.0 &amp;&amp; S.x == 0.0) { u = -T.x/R.x; t = (T.y + u*R.y) / (Q.y + u*S.y); } else if(Q.y == 0.0 &amp;&amp; S.y == 0.0) { u = -T.y/R.y; t = (T.x + u*R.x) / (Q.x + u*S.x); } else { float A = S.x * R.y - R.x * S.y; float B = S.x * T.y - T.x * S.y + Q.x*R.y - R.x*Q.y; float C = Q.x * T.y - T.x * Q.y; // Solve Au^2 + Bu + C = 0 if(abs(A) < 0.0001) u = -C/B; else u = (-B+sqrt(B*B-4.0*A*C))/(2.0*A); t = (T.y + u*R.y) / (Q.y + u*S.y); } u = clamp(u,0.0,1.0); t = clamp(t,0.0,1.0); // These two lines smooth out t and u to avoid visual 'lines' at the boundaries. They can be removed to improve performance at the cost of graphics quality. t = smoothstep(0.0, 1.0, t); u = smoothstep(0.0, 1.0, u); lowp vec4 colorA = mix(color0,color1,u); lowp vec4 colorB = mix(color2,color3,u); gl_FragColor = mix(colorA, colorB, t); }
Result
On the left is the result using smoothstep to smooth t and u, and on the right is the result without it. Although the non-linear smoothstep is computationally-expensive and a hack, I wasn’t happy with the visual lines that resulted in not using it.