One of the biggest advantages of procedural graphics is that simple shapes can be combined to create much more complicated ones.
Instead of drawing an image, we build it one piece at a time.
Think of it like building with LEGO bricks.
One brick is simple.
A handful of bricks can become a house, a car, or even an entire city.
Procedural shaders work in exactly the same way.
Today we'll learn how to combine circles, rings, and gradients to build more complex graphics.
When we created a circle, the result wasn't actually a shape.
It was a number.
float circle = 1.0 - step(0.25, distance(vUv, vec2(0.5)));
Pixels inside the circle received the value 1.0.
Pixels outside received 0.0.
Those numbers can be added, subtracted, or multiplied just like any other values.
That simple idea allows us to build almost anything.
Let's place two circles next to each other.
float circleA = 1.0 - step(
0.18,
distance(vUv, vec2(0.35, 0.5))
);
float circleB = 1.0 - step(
0.18,
distance(vUv, vec2(0.65, 0.5))
);
Each one is created independently.
Now let's combine them.
float shape = circleA + circleB;
Both circles appear together.
Instead of drawing one object, we've created two.
Adding procedural shapes is similar to placing stickers on top of each other.
If either shape exists, the final result also exists.
Addition creates a union of shapes.
This is one of the most common techniques in procedural graphics.
What happens if we subtract one shape from another?
float shape = circleA - circleB;
Where the second circle overlaps the first, part of the shape disappears.
This technique is useful for cutting holes, creating crescents, and designing more interesting silhouettes.
Multiplication behaves differently.
float shape = circleA * circleB;
Only the overlapping area remains visible.
Everything else disappears.
This creates an intersection between the two shapes.
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 vUv;
void main(){
float circleA = 1.0 - step(
0.18,
distance(vUv, vec2(0.40, 0.5))
);
float circleB = 1.0 - step(
0.18,
distance(vUv, vec2(0.60, 0.5))
);
float shape = circleA + circleB;
gl_FragColor = vec4(vec3(shape), 1.0);
}
Although each circle is simple, together they begin to form more interesting graphics.
Move the circles closer together.
vec2(0.45, 0.5)
vec2(0.55, 0.5)
Now they overlap.
Instead of looking like two separate circles, they begin to resemble a capsule.
This simple trick appears in many procedural user interface elements.
The same technique works with rings.
float ringA = outerA - innerA;
float ringB = outerB - innerB;
float shape = ringA + ringB;
Now two outlined circles appear together.
This can become a radar display or a futuristic interface.
Earlier we created glowing circles.
Now combine them.
float glow = glowA + glowB;
The light naturally blends where the two glows overlap.
Many lighting effects begin with exactly this idea.
Nothing says every shape must move the same way.
vec2 centerA = vec2(
0.35 + sin(uTime) * 0.10,
0.5
);
vec2 centerB = vec2(
0.65,
0.5 + cos(uTime) * 0.10
);
Each circle now moves independently.
By combining animated shapes, much richer effects become possible.
Combining procedural shapes is one of the foundations of shader programming.
It appears in
Many complex designs begin with only circles, rectangles, and gradients.
Create two circles.
circleA + circleB
Subtract one circle.
circleA - circleB
Show only the overlap.
circleA * circleB
Animate one circle.
sin(uTime)
Animate both circles independently.
sin(uTime)
cos(uTime)
Try changing their positions, sizes, and colours.
You'll be surprised how many unique designs you can create.
Can you create these effects?
Remember that every effect starts with a few simple building blocks.
Today we discovered that procedural shapes can be combined using ordinary mathematical operations.
Adding shapes creates unions.
Subtracting shapes cuts holes.
Multiplying shapes keeps only overlapping regions.
These techniques allow simple primitives to grow into much more complicated graphics.