Kaleidoscopes work by reflecting light in at least two angled mirrors to form symmetrical patterns. When looking through a kaleidoscope, it is often possible to rotate the portion containing the mirrors so that the image and its reflections shift. In todayâs short article, weâll create a shader that mimics the behaviour of a kaleidoscope by making use of polar coordinates.
Image by Lisa Yount from Skitterphoto
Polar coordinates
In geometry, there are many ways to represent coordinates. Youâre likely most familiar with the Cartesian coordinate system, where points are defined relative to two or more perpendicular axes which meet at an origin point. An alternative two-dimensional system, called polar coordinates, instead represents points by a distance, r, from the origin point and an angle, from a reference direction. The point (1, 1) in the Cartesian system can be represented as and in the polar coordinate system. Why is this helpful for us?
A kaleidoscope reflects images across one or two mirrors. Because of the angles of each mirror, the images self-reflect, resulting in radial symmetry. By converting an image represented by a traditional Cartesian coordinate system into polar coordinates, it becomes much easier to pick out a radial image segment and reflect it across the virtual mirrors.
Letâs jump into the shader, found at Resources/Shaders/Kaleidoscope.shader. Weâll only need to pass in one additional variable to our shader to denote the number of reflections we want - Iâve called it _SegmentCount
. Each segment in this context will contain a single reflection - so weâll end up with two times _SegmentCount
image fragments.
float _SegmentCount;
The first step in the fragment shader is to convert the image to polar coordinates. In order to do that, weâll shift the UVs of the image so the âorigin pointâ is in the centre rather than the corner - all UVs will be between -0.5 and 0.5 instead of between 0 and 1. Then, weâll use Pythagorasâ Theorem to determine the distance of the pixel from the origin, r, and simple trigonometry to determine the angle, . Theyâre stored in the variables radius
and angle
respectively.
// Convert to polar coordinates.
float2 shiftUV = i.uv - 0.5;
float radius = sqrt(dot(shiftUV, shiftUV));
float angle = atan2(shiftUV.y, shiftUV.x);
We also need to know the angle taken up by a single segment. Weâre going to use this to determine which segment the current pixel is in, then âreflectâ the pixel position across the segment boundaries until we are in the âfirstâ segment. Letâs go step by step:
// Calculate segment angle amount.
float segmentAngle = UNITY_TWO_PI / _SegmentCount;
Weâre working in radians rather than degrees, so there are radians in a full circle. Unity provides the built-in variable UNITY_TWO_PI
for us, so all we must do is divide that by _SegmentCount
to get the segmentAngle
.
// Calculate which segment this angle is in.
angle -= segmentAngle * floor(angle / segmentAngle);
Next, we take the pixelâs angle
and subtract segmentAngle
until we are inside the âfirst segmentâ. Itâs essentially a modulus operation.
// Each segment contains one reflection.
angle = min(angle, segmentAngle - angle);
Now that we know our position relative to a single segment, letâs talk about what one image segment looks like. The full image will look like a single segment copy-pasted in a circle, where each segment is a wedge shape. If we converted back to Cartesian coordinates now, then the kaleidoscope wonât look right - there wonât be any reflection. Therefore, each segment must reflect itself through the middle. Using the min
function, weâll keep the angle unchanged if it is less than halfway through a segment, otherwise weâll mirror it across the centre of the segment by subtracting it from segmentAngle
.
Now we can convert back to Cartesian coordinates. We havenât sampled the image yet - to do that, weâll need our UVs back in the classic Cartesian format. Weâll do the inverse of all the transformations we made previously - weâll use cos
and sin
on the angle
to get back the x
and y
components of the float2
respectively, multiply by the radius
to place them at the correct position, and add 0.5 to put the origin point back where it belongs.
// Convert back to UV coordinates.
float2 uv = float2(cos(angle), sin(angle)) * radius + 0.5f;
Thereâs one remaining problem - because of the way we performed the rotational symmetry, some pixels will now be sampling outside the original image. These pixels are at the edges of the image, and we need to shift them back; while we do so, weâll make these edge sections reflect the inner sections of the image.
// Reflect outside the inner circle boundary.
uv = max(min(uv, 2.0 - uv), -uv);
Now we have out final UV coordinates, weâll sample the texture and call it a day.
return tex2D(_MainTex, uv);
Weâll look at the script used to control this effect briefly, as it only needs to pass a single variable representing the variable count to the shader.
[SerializeField]
private int segments = 4;
// Find the Kaleidoscope shader source.
public override void OnCreate()
{
baseMaterial = new Material(Resources.Load<Shader>("Shaders/Kaleidoscope"));
baseMaterial.SetFloat("_SegmentCount", segments);
}
public override void Render(RenderTexture src, RenderTexture dst)
{
Graphics.Blit(src, dst, baseMaterial);
}
Now we can see what the effect looks like. If you plug in Effects/Kaleidoscope and fiddle around with the number of segments, you can create all kinds of effects, like a six-segment star:
Or with three segments, you can sort of create an abstract Unity logo:
Conclusion
Kaleidoscopes are often used as a novelty toy, but they can be used to create striking effects. We can use polar coordinates to deal with the radial symmetries that kaleidoscopes rely on far easier than we can with traditional coordinate systems.
In the next article, weâll make an exciting leap and explore how Lucas Pope created the unique dithering aesthetic for Return of the Obra Dinn.
Acknowledgements
Assets
This tutorial series uses the following asset packs:
Forest - Low Poly Toon Battle Arena / Tower Defense Pack | AurynSky |
Supporters
Support me on Patreon or buy me a coffee on Ko-fi for PDF versions of each article and to access certain articles early!
Special thanks to my Patreon backers:
- Gemma Louise Ilett
- Jack Dixon
- John Selig
- Shaun Wall
- Chris Sims
- Christopher Pereira
- JacksonG
- Pat
And a shout-out to my top Ko-fi supporters:
- Hung Hoang
- Mysterious Anonymous Person