UV Shader Nodes


This post provides several reference plots of (mostly UV-related) shader nodes within Unity, with their implementation formulas as well as the ability to hover over the plots to see the exact values output by these nodes. Importantly, hovering allows you to inspect values outside of the 0.0 to 1.0 display range! These examples all come from Unity’s shader graph system, though other systems (e.g. Unreal’s material editor or Blender’s shading editor) have similar nodes where the same intuition would apply.

This is largely for my own reference/experimentation. While I’ve tried to make sure all the data and behaviors are as close to correct as possible, some edge-cases may not be handled correctly.

Unlike the other nodes here, the UV node is not a function or a transformation, it’s just represents raw data. Every mesh that has been set up for texturing will have a pair of numbers (called U and V) associated with every point on the surface of the model. Intuitively, these numbers are like coordinates describing where you are on the 2D surface of a 3D object. A bit like how latitude and longitude can act as a 2D representation of where you are on the surface of the Earth (ignoring altitude). It’s important to note that these coordinates are completely arbitrary: they’re decided by whoever made the mesh to begin with, and there can be more than one set of UVs for a single model.

Even though UVs are mesh-dependent, shaders are not (a single shader can be applied to many different meshes). Therefore the depiction of UV data as a shader node is generic. U values are represented with the red channel and are mapped to the x-axis, going from 0.0 (left) to 1.0 (right). Likewise, V values are represented with the green channel and are mapped to the y-axis, going from 0.0 (bottom) to 1.0 (top). Click or hover over the UV node below to see the corresponding U (red) and V (green) values associated with any given point.

UV

(+0.00, +0.00)

All other nodes presented below take this UV data as an input and provide some sort of transformed output. Sometimes the transformed values will land outside of the original 0.0 to +1.0 range, in these cases, no color will be rendered, though you can hover to see the underlying values.

Alternatively, the Input control on each node allows you to use the UV map to sample from an image so that the distorting effects of the nodes can be seen in a more intuitive way. For example, if a given point has a UV value of (0.25, 0.5), then that acts as an instruction to replace that point with the pixel from the selected image located 25% of the way along the x-axis and 50% of the way along the y-axis. For the base UV node above, selecting an image results in seeing the (undistorted) image itself, since U and V map directly to x and y sampling coordinates in this case.

These nodes aren’t specific to UV data, but they’re important enough to include here for reference. The Add node provides a scrolling effect when used with image data. The Multiply node provides scaling effects unsurprisingly, but the behavior is counter-intuitive when working with image data since decreasing the multiplication factor increases the size of the image. This makes sense if you take some time to think about it, but it’s easy to get this backwards, a bit like a cognitive reflection test.

Add

(+0.00, +0.00)

Multiply

(+0.00, +0.00)

This node provides convenient outputs for rotation-based effects. The red channel outputs the radial distance from the Center coordinate, while the green channel represents rotation from the +y-axis. By default, this node generates negative values, which can result in a somewhat confusing looking plot. For example, with default settings, the rotation data (green channel) ranges from -0.5 to +0.5 which produces an image with very little green coloring since only the 0.0 to +0.5 values are displayed. However, if you hover over the left half of the plot, you can see how the underlying rotation (green) data is actually varying.

Polar Coordinates

(+0.00, +0.00)

Shader function:

hlsl
// Get Centered UVs
float2 delta = UV - Center;
// Calculate polar output components
float radius = length(delta) * RadialScale * 2.0;
float angle = atan2(delta.x, delta.y) * LengthScale * (1.0 / TWO_PI);
return float2(radius, angle);

This node rotates all values around a given pivot point by a given rotation angle. This one is conceptually straight forward, but if you move the center point at all, it’s very easy to end up with incomprehensible UV values due to rotating in values outside of the 0.0 to 1.0 range.

Rotate (Degrees)

(+0.00, +0.00)

Shader function:

hlsl
// Get Centered UVs
float2 delta = UV - Center;
// Calculate rotation terms
float rotation_radians = Rotation * PI / 180.0;
float sin_term = sin(rotation_radians);
float cos_term = cos(rotation_radians);
// Calculate rotated coords. (2D rotation)
float x_rot = delta.x * cos_term + delta.y * sin_term;
float y_rot = -delta.x * sin_term + delta.y * cos_term;
// Reset positioning
float out_x = x_rot + Center.x;
float out_y = y_rot + Center.y;
return float2(out_x, out_y);

Fun looking! I’m sure it could be used for some interesting artistic effects. It’s similar to the rotation node, except that the rotation angle is reduced to zero as you get closer to the central pivot point.

This node (and several others) contains an Offset control, though a more intuitive name would be Scroll. It acts exactly like having an Add node following the twirl effect, which may seem odd to have built-in to the node, but the result is pretty cool. Try it on image data to see the full effect.

Twirl

(+0.00, +0.00)

Shader function:

hlsl
// Get Centered UVs
float2 delta = UV - Center;
// Calculate rotation terms
float angle = Strength * length(delta);
float cos_term = cos(angle);
float sin_term = sin(angle);
// Calculate twirled coords. (2D rotation)
float x_twirl = cos_term * delta.x - sin_term * delta.y;
float y_twirl = sin_term * delta.x + cos_term * delta.y;
// Reset positioning and apply offsets for final output
float out_x = x_twirl + Center.x + Offset.x;
float out_y = y_twirl + Center.y + Offset.y;
return float2(out_x, out_y);

It would be fair to call this the stair step node or even a pixelate node. It’s not specific to UV data, but the effect is very easy to see with UVs. It converts a continuous range of values into a small number of equally sized regions of constant value. Intuitively, the Steps input determines how many regions you’ll get in the 0.0 to 1.0 range. It’s worth noting that a step value of 0 results in undefined behavior (Unity renders a magenta image in these cases).

Posterize

(+0.00, +0.00)

Shader function:

hlsl
return floor(Value * Steps) / Steps;

While quite useful, the tiling and offset node exhibits some unintuitive behavior at first glance. For example, the UV plot doesn’t appear to tile at all! In fact, it would be more correct to call it a scaling and offset node, or even a y = mx + b node, since that’s exactly what it does. However, it really does have a tiling effect when used to sample textures, since sampling seems to have a built-in wrap-around behavior.

Tiling and Offset

(+0.00, +0.00)

Shader function:

hlsl
return (Value * Tiling) + Offset;

Combining a fraction node with the tiling and offset node mentioned above gives the behavior you might expect from the name tiling and offset. For positive numbers, the fraction node gives the fractional part of the input (i.e. given 2.73 as an input, the output from the fraction node would be 0.73). For negative numbers the behavior is similar though a bit less intuitive. Nevertheless, this is a surprisingly useful node, and definitely worth remembering.

Fraction Tiling and Offset

(+0.00, +0.00)

Shader function (fraction only):

hlsl
return Value - floor(Value);

This node doesn’t make a lot of sense from the UV plot, but is very intuitive when applying it to an image. As with the twirl node, there is an Offset control, which can create a very interesting scrolling effect. It’s most likely useful for VFX. Try it with negative strength values!

Spherize

(+0.00, +0.00)

Shader function:

hlsl
// Get Centered UVs
float2 delta = UV - Center;
// Calculate spherize components
float length_squared = dot(delta.xy, delta.xy);
float length_to_4th_power = length_squared * length_squared;
float2 sphere_offset = delta * Strength * length_to_4th_power;
return UV + sphere_offset + Offset;

This is another node that only seems to make sense with image data. Varying the Offset property gives some pretty wild looking distortion that’s probably useful for VFX. Maybe for flowing water?

Radial Shear

(+0.00, +0.00)

Shader function:

hlsl
// Get Centered UVs
float2 delta = UV - Center;
// Calculate shear scaling
float length_squared = dot(delta.xy, delta.xy);
float2 shear_scale = Strength * length_squared;
// Calculate shear components
float x_shear = delta.y * shear_scale;
float y_shear = -delta.x * shear_scale;
float2 xy_shear = float2(x_shear, y_shear);
return UV + xy_shear + Offset;

This node definitely has the most specific use-case of all other nodes presented here. It’s purpose is to subsample rectangular regions of the UV space. If an image is used, then it provides the cropping you might need for a texture altas/spritesheet/tile set.

The somewhat confusingly named Width and Height controls are used to specify the number of tiles in the x and y directions (respectively) across the UV space. It might be easier to think of these inputs as the number of Columns and Rows instead. The Tile control is a 1D index into the 2D grid of tiles, in row-major order. The inverting controls determine where the tiling begins as well as the ordering sequence.

Try switching to the flipbook image input and then changing the Tile control to see the effect. Make sure to also set Width to 5 and Height to 2 to account for the tiling within the image.

Flipbook

(+0.00, +0.00)

Shader function:

hlsl
// Calculate tile sizing in UV units
// Note Width/Height controls actually represent
// the number of columns and rows, respectively
float tile_width = 1.0 / Width; // i.e. 1.0 / Num Columns
float tile_height = 1.0 / Height; // i.e. 1.0 / Num Rows
// Figure out the grid indexing of the selected tile
float col_idx = floor(Tile) % Width;
float row_idx = floor(Tile * tile_width) % Height;
// Find x offset needed to crop the selected tile
float tile_offset_x = col_idx * tile_width;
if (InvertX) tile_offset_x = 1.0 - tile_width * (1.0 + col_idx);
// Find y offset needed to crop the selected tile
float tile_offset_y = row_idx * tile_height;
if (InvertY) tile_offset_y = 1.0 - tile_height * (1.0 + row_idx);
// Apply scaling & offsets for output
float out_x = (u * tile_width) + tile_offset_x;
float out_y = (v * tile_height) + tile_offset_y;
return float2(out_x, out_y);

To help clarify the inverting behavior, the diagrams below show how the Tile index samples from it’s grid (with Width = 3 and Height = 2 in this case) with each of the different inversion settings. The large circles indicate where the first tile would be located (i.e. Tile = 0), with the arrows showing how the grid cells are indexed as Tile increases. The indexing wraps back to the start if the Tile value increases past the number of grid cells.

Flipbook default ordering Flipbook ordering: invert x Flipbook ordering: invert y Flipbook ordering: invert x and invert y

Warning: The implementation above deviates from Unity’s shader graph implementation when using fractional tile sizes (i.e. Width or Height values that are not integers). The code and implementation presented here is focused on providing an intuitive grid-based description of the behavior. While Unity’s implementation appears grid-based with integer values, it does something quite a bit more complex when switching to fractional values. I’m not sure that non-integer values are relevant in practice, so I don’t think this would be much of an issue, however, if you do use non-integer sizes just be sure to test the output directly within Unity!

Unlike the other UV-related nodes, the triplanar node seems to depend on mesh/normals data, so I haven’t included it here. I may revisit this node in the future, once I’m more familiar it’s operation and have some interesting sample data to work from!