|
| 1 | +# Circles of light |
| 2 | +Ho ho, shader friends! |
| 3 | +Today, I have a Christmas tale to share—a tale of circles. |
| 4 | +Not just any circles, but circles that begin as humble shapes and evolve into radiant, magical rings of light. |
| 5 | +But let’s not skip ahead. |
| 6 | +The story begins with me agreeing to write this article about shader coding. |
| 7 | +“Me? Is this really a good idea?” I wondered, considering I’m still a beginner in the world of shaders? |
| 8 | +Days passed, the deadline loomed, and progress... well, it didn’t. |
| 9 | + |
| 10 | +One evening, in a fit of procrastination, I lazily searched YouTube for shader tutorials. |
| 11 | +That’s when I stumbled upon [An Introduction to Shader Art Coding by Kishimisu](https://www.youtube.com/watch?v=f4s1h2YETNY). |
| 12 | +Wow. Starting with the simplest of circles, |
| 13 | +Kishimisu used ingenious techniques to transform them into dazzling, animated works of art. |
| 14 | + |
| 15 | +Inspired, I thought, “Could I pull off something similar but with my own twist?” |
| 16 | + |
| 17 | +## The basic idea |
| 18 | + |
| 19 | +It turns out that it is very simple to draw a circle using a shader. |
| 20 | + |
| 21 | +```glsl |
| 22 | +void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
| 23 | +{ |
| 24 | + vec2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y; |
| 25 | + vec2 center = vec2(0.0,0.0); |
| 26 | + float radius = 0.7; |
| 27 | +
|
| 28 | + // Calculate distance to circle. Use smoothstep |
| 29 | + // to get smooth edges. |
| 30 | + float dist = |
| 31 | + smoothstep( |
| 32 | + 0.0, |
| 33 | + 0.05, |
| 34 | + abs(length(uv + center) - radius) |
| 35 | + ); |
| 36 | + |
| 37 | + fragColor = vec4(dist, dist, dist, 1.0); |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +<img src="assets/single circle.png" width="250px"> |
| 42 | + |
| 43 | +A single circle is not interesting enough though but what about a circles that contains other circles? |
| 44 | + |
| 45 | +If $r_n$ is the radius of the outer circle and $c_n$ is the center we can calculate the radius of the inner circle by multiplying by a number less than one, for example |
| 46 | + |
| 47 | +$r_{n+1} = r_{n} * 0.8$ |
| 48 | + |
| 49 | +If we want the inner circle to touch the outer, the new center will be |
| 50 | + |
| 51 | +$c_{n+1} = c_n + (r_{n} - r_{n+1}) \times [cos(\alpha), sin(\alpha)]$ |
| 52 | + |
| 53 | +where $\alpha$ is the rotation of the inner circle around the center of the outer. Let's try that out. |
| 54 | + |
| 55 | +```glsl |
| 56 | +void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
| 57 | +{ |
| 58 | + vec2 uv = ...; |
| 59 | + |
| 60 | + float r_0 = ...; |
| 61 | + vec2 c_0 = ...; |
| 62 | + float dist_0 = smoothstep(...); |
| 63 | +
|
| 64 | + float r_1 = r_0 * 0.8; |
| 65 | + float alpha = ...; // Arbitrary rotation angle |
| 66 | + vec2 c_1 = c_0 + (r_0 - r_1) * vec2(cos(alpha), sin(alpha)); |
| 67 | + float dist_1 = smoothstep(...); |
| 68 | +
|
| 69 | + // The combined distance is the minimum distance of the parts |
| 70 | + float dist = min(dist_0, dist_1); |
| 71 | + |
| 72 | + fragColor = ...; |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +<img src="assets/two_nested_circles.png" width="300px" /> |
| 77 | + |
| 78 | + |
| 79 | +Using a for loop we can create as many as we want. |
| 80 | + |
| 81 | +```glsl |
| 82 | +void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
| 83 | +{ |
| 84 | + vec2 uv = ...; |
| 85 | + |
| 86 | + float ro = ...; |
| 87 | + vec2 co = ...; |
| 88 | + float dist = ...; |
| 89 | + |
| 90 | + for(float i=1.0; i<5.0; i++) |
| 91 | + { |
| 92 | + float ri = ...; |
| 93 | + float alpha = ...; |
| 94 | + vec2 ci = ...; |
| 95 | + float disti = ...; |
| 96 | + dist = min(disti, dist); |
| 97 | + |
| 98 | + ro = ri; |
| 99 | + co = ci; |
| 100 | + } |
| 101 | + |
| 102 | + fragColor = vec4(dist, dist, dist, 1.0); |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +<img src="assets/many_nested_circles.png" width="250px" /> |
| 107 | + |
| 108 | +Still no magic but maybe it could be interesting enough using some tricks? |
| 109 | + |
| 110 | +## Trick 1: Animation |
| 111 | + |
| 112 | +It is easy to animate by letting the rotation $\alpha$ vary by time and circle index. |
| 113 | + |
| 114 | +```glsl |
| 115 | +float alpha = <arbitrary constant> * i * iTime; |
| 116 | +``` |
| 117 | + |
| 118 | +## Trick 2: A nice color palette |
| 119 | + |
| 120 | +Using the general palette function from https://iquilezles.org/articles/palettes/ combined with Kishimisu's parameter selection I got this |
| 121 | + |
| 122 | +```glsl |
| 123 | +vec3 palette( in float t ) |
| 124 | +{ |
| 125 | + vec3 a = vec3(0.5, 0.5, 0.5); |
| 126 | + vec3 b = vec3(0.5, 0.5, 0.5); |
| 127 | + vec3 c = vec3(1.0, 1.0, 1.0); |
| 128 | + vec3 d = vec3(0.263, 0.416, 0.557); |
| 129 | + return a + b*cos( 6.283185*(c*t+d) ); |
| 130 | +} |
| 131 | +
|
| 132 | +void mainImage( out vec4 fragColor, in vec2 fragCoord ) |
| 133 | +{ |
| 134 | + ... |
| 135 | +
|
| 136 | + vec3 color = |
| 137 | + palette( |
| 138 | + length(uv) + iTime * <arbitrary constant> |
| 139 | + ) * (1.0 - dist); |
| 140 | + fragColor = vec4(color, 1.0); |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +Again, time is used to animate the effect. |
| 145 | + |
| 146 | +## Trick 3: Color saturation overload |
| 147 | + |
| 148 | +A very simple and cheap trick to get more saturated colors seems to be to subract a little. I do not know why it works but I tried, and I liked the result. |
| 149 | + |
| 150 | +```glsl |
| 151 | +dist -= 0.5; // Magic: Subtract a little for more saturation |
| 152 | +vec3 color = palette(...) * (1.0 - dist); |
| 153 | +``` |
| 154 | + |
| 155 | +## Trick 4: Warp the plane by a pinch of noise |
| 156 | + |
| 157 | +At this point the result look as below. |
| 158 | + |
| 159 | +<img src="assets/without-noise.png" width="300px" /> |
| 160 | + |
| 161 | +Colorful, but a bit boring. What if I could warp the plane with some noise to make it more interesting? |
| 162 | + |
| 163 | +Searching for noise effects on shadertoy.com did not disappoint. I copied to noise function from [Warping - procedural 2](https://www.shadertoy.com/view/lsl3RH). |
| 164 | + |
| 165 | +```glsl |
| 166 | +const mat2 m = mat2( 0.80, 0.60, -0.60, 0.80 ); |
| 167 | +
|
| 168 | +float noise( in vec2 p ) |
| 169 | +{ |
| 170 | + return sin(p.x)*sin(p.y); |
| 171 | +} |
| 172 | +
|
| 173 | +float fbm4( vec2 p ) |
| 174 | +{ |
| 175 | + float f = 0.0; |
| 176 | + f += 0.5000*noise( p ); p = m*p*2.02; |
| 177 | + f += 0.2500*noise( p ); p = m*p*2.03; |
| 178 | + f += 0.1250*noise( p ); p = m*p*2.01; |
| 179 | + f += 0.0625*noise( p ); |
| 180 | + return f/0.9375; |
| 181 | +} |
| 182 | +
|
| 183 | +vec2 fbm4_2( vec2 p ) |
| 184 | +{ |
| 185 | + return vec2(fbm4(p), fbm4(p+vec2(7.8))); |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Using this noise function it was just a matter of mixing it with the uv-coordinates. |
| 190 | + |
| 191 | +```glsl |
| 192 | +vec2 uv = ...; |
| 193 | +float warpAmount = 0.75; // rather arbitrary |
| 194 | +uv = mix( |
| 195 | + fbm4_2(uv + iTime * <arbitrary constant>), |
| 196 | + uv, |
| 197 | + warpAmount |
| 198 | +); |
| 199 | +``` |
| 200 | + |
| 201 | +The result looked like below. |
| 202 | + |
| 203 | +<img src="assets/with-noise.png" width="300px" /> |
| 204 | + |
| 205 | +# Summary |
| 206 | + |
| 207 | +The final shader, after some tweaking of the constants, can be seen [here](https://www.shadertoy.com/view/McdcWM). |
| 208 | +With only a basic understanding and a few simple concepts, I managed to create a vibrant, colorful effect. Sure, it carries the mark of a beginner, but it’s mine—and it’s unique. It feels like there’s an entire universe of undiscovered shaders waiting to be explored. Good times. Wishing you a bright and creative St. Lucia’s Day—and a joyful Advent! |
| 209 | + |
| 210 | +*Abstract Dan, 2024* |
0 commit comments