mathematics – Help understanding radar sweep effect in shader code

  1. What is the range of values for the parameters i.uv.y or i.uv.x?

Usually a quad’s texture coordinates will run from 0 to 1 on the u (x) axis, and 0 to 1 on the v (y) axis.

That matches the normalized sampling coordinates we use for looking up into a texture:

  • (0, 0) = bottom left
  • (1, 1) = top right

(In some contexts, 0 is the top instead of the bottom, but let’s call it the bottom here for simplicity)

But in this code, you can see I remap the range inside the vertex shader:

v2f vert (appdata v)
     v2f o;
     o.vertex = UnityObjectToClipPos(v.vertex);
     // Shift our texture coordinates so 0 is in the center,
     // and we go to -1 ... +1 at the edges.
     o.uv = (v.uv - 0.5f) * 2.0f;
     return o;

This output structure o gets interpolated for each pixel in the triangle formed by three vertices, and those interpolated versions get passed to the fragment shader as the input structure i.

By subtracting 0.5, I ensure the middle of the quad becomes the origin (0, 0). Then by scaling by 2, I ensure a circle with radius 1 centered at the origin exactly touches the edges of the quad.

Then in the fragment shader, we convert the texture coordinates of the pixel we’re drawing into polar coordinates. This code gives us the angle of our pixel around the center of the quad, measured counter-clockwise from the positive u (x) axis.

// Compute the angle of the pixel we're rendering.
float angle = atan2(i.uv.y, i.uv.x);

That’s why, when you plot this value as a greyscale colour, you get a gradient that gets brighter as you look around the quad in a counter-clockwise direction – that’s the measured angle to this pixel.

As you reasoned, since the range of atan2 is from $-pi$ to $pi$, dividing this by $2 pi$ gives us a range from -0.5 to 0.5. The frac function then takes the negative values and wraps them around to positive. We can visualize this as a graph in the angular domain:

Graphs of angle/2pi and frac(angle/2pi)

So here we have a function that varies over space with the angle of the pixel – from 0 (black) at the start of quadrant I to 1 (white) at the end of quadrant IV. But it doesn’t vary over time.

The next bit of code you tried does the opposite: it varies over time, but it doesn’t vary across space.

float headAngle = _Time.y;
float difference = frac(headAngle)

_Time is a built-in variable that Unity populates with various different scaled measures of the time elapsed in the game (the y component happened to contain a convenient scale for the effect I was making). So its value constantly increases from one frame to the next. But within a single frame, it’s a constant – no matter what pixel looks up _Time.y, they’ll all get the same value. That makes a graph that looks like this:

Graph of horizontal line for time. Ghosted lines indicate the level is rising.

Here I’ve visualized multiple consecutive frames. You can see in any one frame, _Time.y is a constant, horizontal line in this graph: it’s the same at any angle. But from one frame to the next that line rises. Eventually it hits the top of the frac range and wraps back down to zero, then rises again.

Now we’re ready to put these two pieces together. Let’s take our red plot of frac(angle/(2*pi)) we had before, and imagine what happens when we add this lifting influence of time to it:

Graphs of frac(angle/(2pi)) + Time and frac(angle/(2pi) + Time)

Increasing the time offset lifts the whole graph up. But then frac cuts off any parts that go above 1 and wraps them back around to the bottom.

Each time we lift the line, more of the right (counter-clockwise) side of it goes above 1, so we trim that part off and move it down to zero. Which means that effectively the peak value of the graph moves to the left (clockwise) as time goes on.

That’s what makes our radar-like gradient sweep around the circle.