This is the first tutorial in a series about creating the appearance of flowing materials. In this case, it's done by using a flow map to distort a texture. This tutorial assumes you've gone through the Basics series, plus the Rendering series up to at least part 6, Bumpiness.

This tutorial is made with Unity 2017.4.4f1.

While that is possible, flow maps often cover large areas and thus end up with low effective resolution. As long as you're not using extreme deformation, there is no problem. The deformations shown in this tutorial are very strong, to make them visually obvious.

We can now see that the texture indeed gets deformed in different directions and at different speeds. Besides the sudden reset, what is most obvious is that the texture quickly becomes blocky as its deformation increases. This is caused by the compression of the flow map. The default compression setting uses the DXT1 format, which is where the blockiness comes from. These artifacts are typically not obvious when using organic textures, but are glaring when deforming clear patterns, like our test texture. So I've used an uncompressed flow map for all screenshots and movies in this tutorial.

As this is particular to the flow animation and not time in general, create the sawtooth progression in FlowUV .

We quickly end up with a texture that is way too distorted. This happens because the texture gets moved in multiple directions, stretching and squashing it more and more as time progresses. To prevent it from turning into a mess, we have to reset the animation at some point. The simplest way to do this is by only using the fractional part of the time for the animation. Thus, it progresses from 0 up to 1 as normal, but then resets to 0, forming a sawtooth pattern.

Pass the flow vector to the function, but before doing so make sure that the vector is valid. Like with a normal map, the vector can point in any direction, so can contain negative components. Therefore, the vector is encoded the same way as in a normal map. We have to manually decode it. Also, revert to the original albedo.

Now that we have flow vectors, we can add support for them to our FlowUV function. Add a parameter for them, then multiply them with the time before subtracting from the original UV. We subtract because that makes the flow go in the direction of the vector.

The texture appears brighter in the scene, because it's linear data. That's fine, because we're not supposed to use it as a color anyway. As the main UV coordinates of the surface shader use the tiling and offset of the main texture, our flow map gets tiled as well. We don't need a tiling flow map, so set the material's tiling back to 1.

Add a variable for the flow map and sample it to get the flow vector. Then temporarily visualize it by using it as the albedo.

Add a property for the flow map to our material. It doesn't need a separate UV tiling and offset, so give it the NoScaleOffset attribute. The default is that there is no flow, which corresponds to a black texture.

This texture was created with curl noise, which is explained in the Noise Derivatives tutorial, but the details of its creation don't matter. It contains multiple clockwise and counterclockwise rotating flows, without any sources or sinks. Make sure that it is imported as a regular 2D texture that isn't sRGB, as it doesn't contain color data.

To support more interesting flows, we must somehow vary the flow vector across the surface of our material. The most straightforward way to do this is via a flow map. This is a texture that contains 2D vectors. Here is such a texture, with the vector's U component in the R channel and the V component in the G channel. It doesn't need to be large, because we don't need sharp sudden changes and we can rely on bilinear filtering to keep it smooth.

We could get rid of the static appearance by adding another velocity vector, using that to sample the texture a second time, and combining both samples. When using two slightly different vectors, we end up with a morphing texture. However, we're still limited to flowing the entire surface the same way. This is often sufficient for open water or straight flows, but not in more complex situations.

Instead of always flowing in the same direction, you can use a velocity vector to control the direction and speed of the flow. You could add this vector as a property to the material. However, then we're still limited to using the same vector for the entire material, which looks like a rigid sliding surface. To make make something look like flowing liquid, it has to locally change over time besides moving in general.

Actually, the time value used by materials increases each time the editor redraws the scene. So when Animated Materials is disabled you will see the texture slide a bit each time you edit something. Animated Materials just forces the editor to redraw the scene all the time. So only turn it on when you need it.

The animation is only visible when the time value increases. This is the case when the editor is in play mode, but you can also enable time progression in edit mode, by enabling Animated Materials via the Scene window toolbar.

As we're increasing both coordinates by the same amount, the texture slides diagonally. Because we adding the time, it slides from top right to bottom left. And because we're using the default wrap mode for our texture, the animation loops every second.

Include this file in our shader and invoke FlowUV with the main texture coordinates and the current time, which Unity makes available via _Time.y . Then use the new UV coordinates to sample our texture.

The code for flowing UV coordinates is generic, so we'll put it in a separate Flow.cginc include file. All it needs to contain is a FlowUV function that has a UV and a time parameter. It should return the new flowed UV coordinates. We begin with the most straightforward displacement, which is simply adding the time to both coordinates.

Create a material that uses our shader, with the test texture as its albedo map. Set its tiling to 4 so we can see how the texture repeats. Then add a quad to the scene with this material. For best viewing, rotate it 90° around its X axis so it lies flat in the XZ plane. That makes it easy to look at it from any angle.

To make it easy to see how the UV coordinates are deformed, you can use this test texture .

For this tutorial, you can start with a new project, set to use linear color space rendering. If you're using Unity 2018, select the default 3D pipeline, not lightweight or HD. Then create a new standard surface shader. As we're going to simulate a flowing surface by distorting texture mapping, name it DistortionFlow. Below is the new shader, with all comments and unneeded parts removed.

The technique used in this tutorial was first publicly described in detail by Alex Vlachos from Valve, in the SIGGRAPH2010 presentation Water Flow in Portal 2.

Most of the time, we just want a surface to be made out of water, or mud, or lava, or some magical effect that visually behaves like a liquid. It doesn't need to be interactive, just appear believable when casually observed. So we don't need to come up with a complex water physics simulation. All we need is some movement added to a regular material. This can be done by animating the UV coordinates used for texturing.

When a liquid doesn't move, it is visually indistinguishable from a solid. Are you looking at water, jelly, or glass? Is that still pool frozen or not? To be sure, disturb it and observe whether it deforms, and if so how. Merely creating a material that looks like moving water isn't enough, it actually has to move. Otherwise it's like a glass sculpture of water, or water frozen in time. That's good enough for a picture, but not for a movie or a game.

Seamless Looping

At this point we can animate a nonuniform flow, but it resets each second. To make it loop without discontinuity, we have to somehow get the UV back to their original values, before distortion. Time only goes forward, so we cannot rewind the distortion. Trying that would result in a flow that goes back and forth instead of in a consistent direction. We have to find another way.

Blend Weight We cannot avoid resetting the progression of the distortion, but we can try to hide it. What we could do is fade the texture to black as we approach maximum distortion. If we also start with black and fade in the texture at the start, then the sudden reset happens when the entire surface is black. While this is very obvious, at least there is no sudden visual discontinuity. To make the fading possible, let's add a blend weight to the output of our FlowUV function, renaming it to FlowUVW . The weight is put in the third component, which has effectively been 1 up to now, so let's start with that. float3 FlowUVW (float2 uv, float2 flowVector, float time) { float progress = frac(time); float3 uvw; uvw.xy = uv - flowVector * progress; uvw.z = 1; return uvw; } We can fade the texture by multiplying it with the weight that is now available to our shader. float3 uvw = FlowUVW (IN.uv_MainTex, flowVector, _Time.y); fixed4 c = tex2D(_MainTex, uvw.xy ) * uvw.z * _Color;

Seesaw Now we must create a weight function `w(p)` where `w(0) = w(1) = 0`. And halfway it should reach full strength, so `w(1/2) = 1`. The simplest function that matches these criteria is a triangle wave, `w(p) = 1 - |1 - 2p|`. Use that for our weight. Sawtooth with matching triangle wave. uvw.z = 1 - abs(1 - 2 * progress) ; Triangle wave modulation. Why not use a smoother function? You could also use a sine wave or apply the smoothstep function. But those functions make the shader more complex, while not affecting the final result much. A triangle wave is sufficient.

Time Offset While technically we have removed the visual discontinuity, we have introduced a black pulsing effect. The pulsing is very obvious because it happens everywhere at once. It might be less obvious if we could spread it out over time. We can do this by offsetting the time by a varying amount across the surface. Some low-frequency Perlin noise is very suitable for this. Instead of adding another texture, we'll pack the noise in our flow map. Here is the same flow map as before, but now with noise in its A channel. The noise is unrelated to the flow vectors. Flow map with noise in A channel. To indicate that we expect noise in the flow map, update its label. [NoScaleOffset] _FlowMap ("Flow (RG , A noise )", 2D) = "black" {} Sample the noise and add it to the time before passing it to FlowUVW . float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float noise = tex2D(_FlowMap, IN.uv_MainTex).a; float time = _Time.y + noise; float3 uvw = FlowUVW(IN.uv_MainTex, flowVector, time ); Time with offset. Why sample the flow map twice? Just to point out that the shader compiler will optimize that into a single texture sample. The black pulse is still there, but it has changed into a wave that spreads across the surface in an organic way. This is much easier to obfuscate than uniform pulsing. As a bonus, the time offset also made the progression of the distortion nonuniform, resulting in a more varied distortion overall.

Combining Two Distortions Instead of fading to black, we could blend with something else, for example the original undistorted texture. But then we would see a fixed texture fade in and out, which would destroy the illusion of flow. We can solve that by blending with another distorted texture. This requires us to sample the texture twice, each with different UVW data. So we end up with two pulsing patterns, A and B. When A's weight is 0, B's should be 1, and vice versa. That way the black pulse is hidden. This is done by shifting the phase of B by half its period, which means adding 0.5 to its time. But that's a detail of how FlowUVW works, so let's just add a boolean parameter to indicate whether we want the UVW for the A or B variant. float3 FlowUVW (float2 uv, float2 flowVector, float time , bool flowB ) { float phaseOffset = flowB ? 0.5 : 0; float progress = frac(time + phaseOffset ); float3 uvw; uvw.xy = uv - flowVector * progress; uvw.z = 1 - abs(1 - 2 * progress); return uvw; } Weights of A and B always sum to 1. We now have to invoke FlowUVW twice, once with false and once with true as its last argument. Then sample the texture twice, multiply both with their weights, and add them to arrive at the final albedo. float time = _Time.y + noise; float3 uvwA = FlowUVW(IN.uv_MainTex, flowVector, time , false ); float3 uvwB = FlowUVW(IN.uv_MainTex, flowVector, time, true); fixed4 texA = tex2D(_MainTex, uvwA.xy) * uvwA.z; fixed4 texB = tex2D(_MainTex, uvwB.xy) * uvwB.z; fixed4 c = (texA + texB) * _Color; Blending two phases. The black pulsing wave is no longer visible. The wave is still there, but now forms the transition between the two phases, which is far less obvious. A side effect of blending between two patterns offset by half their period is that our animation's duration has been halved. It now loops twice per second. But we don't have to use the same pattern twice. We can offset the UV coordinates of B by half a unit. This makes the patterns different—while using the same texture—without introducing any directional bias. uvw.xy = uv - flowVector * progress + phaseOffset; Different UV for A and B. Because we use a regular test pattern, the white grid lines of A and B overlap. But the colors of their squares are different. As a result, the final animation alternates between two color configurations, and again takes a second to repeat.

Jumping UV Besides always offsetting the UV of A and B by half a unit, it is also possible to offset the UV per phase. That will cause the animation to change over time, so it takes longer before it loops back to the exact same state. We could simply slide the UV coordinates based on time, but that would cause the whole animation to slide, introducing a directional bias. We can avoid visual sliding by keeping the UV offset constant during each phase, and jumping to a new offset between phases. In other words, we make the UV jump each time the weight is zero. This is done by adding some jump offset to the UV, multiplied by the integer portion of the time. Adjust FlowUVW to support this, with a new parameter to specify the jump vector. float3 FlowUVW ( float2 uv, float2 flowVector, float2 jump, float time, bool flowB ) { float phaseOffset = flowB ? 0.5 : 0; float progress = frac(time + phaseOffset); float3 uvw; uvw.xy = uv - flowVector * progress + phaseOffset; uvw.xy += (time - progress) * jump; uvw.z = 1 - abs(1 - 2 * progress); return uvw; } Add two parameters to our shader to control the jump. We use two floats instead of a single vector, so we can use range sliders. Because we're blending between two patterns that are offset by half, our animation already contains the UV offset sequence `0 -> 1/2` per phase. The jump offset gets added on top of this. This means that if we were to jump by half, the progression would become `0 -> 1/2 -> 1/2 -> 0` over two phases, which is not what we want. We should jump by a quarter at most, which produces `0 -> 1/2 -> 1/4 -> 3/4 -> 1/2 -> 0 -> 3/4 -> 1/4` over four phases. A negative offset of at most a quarter is also possible. That would produce the sequence `0 -> 1/2 -> 3/4 -> 1/4 -> 1/2 -> 0 -> 1/4 -> 3/4`. [NoScaleOffset] _FlowMap ("Flow (RG, A noise)", 2D) = "black" {} _UJump ("U jump per phase", Range(-0.25, 0.25)) = 0.25 _VJump ("V jump per phase", Range(-0.25, 0.25)) = 0.25 Add the required float variables to our shader, use them to construct the jump vector, and pass it to FlowUVW . sampler2D _MainTex, _FlowMap; float _UJump, _VJump; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float noise = tex2D(_FlowMap, IN.uv_MainTex).a; float time = _Time.y + noise; float2 jump = float2(_UJump, _VJump); float3 uvwA = FlowUVW(IN.uv_MainTex, flowVector, jump, time, false); float3 uvwB = FlowUVW(IN.uv_MainTex, flowVector, jump, time, true); … }

Material with maximum jump. At maximum jump we end up with a sequence of eight UV offsets before it repeats. As we go through two offsets per phase and each phase is one second long, our animation now loops every four seconds.