Monday, July 16, 2018

Lucky Swooshes

While working on Super Lucky's Tale something that I though was solved in a pretty cool way was the swooshes. Swooshes are used for collecting coins, spawning certain enemies, and Lucky's tail swipe effect. For this type of effect you want a fire and forget solution and you also want it to be super predictable. It should do the same thing every time and keep a consistent look over its lifetime. An obvious approach may be to use a trail attached to an object that moved toward a target. There's a few issues that may arise with this approach though. If the target moves the trail could end up having a funny shape, it could also be difficult to figure out exactly when the swoosh will arrive at the target which doesn't help with timing when to spawn things.
The best solution turned out being a mesh that was a strip of polygons with the beginning and end at 0,0,0 with a trail texture that scrolled across it. A script on the swoosh object informed the shader where the target was and the vertex shader moved the end of the swoosh over to the targets position. The script could also orient the mesh to face the target. This allowed for lots of variation in the shape of the swooshes and also guaranteed that the swoosh would reach the target exactly when it was supposed to and always have the intended shape.
A swoosh model could be made with all kinds of twisting ribbons and then deformed along a path to make all kinds of fun shapes.

The important part of the vertex shader that moves the end of the swoosh is below:
// For screen shooshes smoosh the mesh flat on the Y axis
v.vertex.y *= 1.0 - _ScreenSquish;

// Add some random offset on the X and Z zxis for screen swooshes
float2 divergence = _Divergence.xy * saturate( sin ( v.uv.x * UNITY_PI ) );
v.vertex.xz += divergence * _ScreenSquish;

// Find the start position of the swoosh
float3 worldOrigin = mul( unity_ObjectToWorld, float4(0,0,0,1) ).xyz;

// Now figure out the end position relative to the start position
float3 endOffset = _TargetPos - worldOrigin;

// Get the world position of the vertex
float3 worldPos = mul( unity_ObjectToWorld, v.vertex ).xyz;

// Add the end offset to the vertex world position masked byt the uv coordinates
worldPos += endOffset * v.uv.x;

// Transform the world position to screen position
o.vertex = mul(UNITY_MATRIX_VP, float4(worldPos,1));

// Smoosh the swoosh against the screen if it is a screen swoosh
o.vertex.z = lerp( o.vertex.z, o.vertex.w, _ScreenSquish * 0.99 );
_ScreenSquish is a float from 0-1 that is passed in from script telling the swoosh if it should be pressed against the screen, like the coin collect swooshes. This keeps is from being occluded by any opaque geometry while still being attached to a point in the world. It still follows a world space position but that position is attached to the screen.
_Divergence is a float2 passed in from script that adds some offset the the middle of the swoosh so screen swooshes don't overlap and follow a bit of a random path.

The pixel shader is pretty simple, here's the basic swoosh texture lookup with a bit of fade out on either end:
// texture coords for swoosh texture
float2 swooshUV = saturate( IN.uv * _Tiling.xy + float2( lerp( _Tiling.z, _Tiling.w, _Ramp ), 0 ) );
half4 col = tex2D(_MainTex, swooshUV ) * _Color;

// start and end fade in
half edgeFade = saturate( ( 1.0 - abs( IN.uv.x * 2 - 1 ) ) * (1.0 / _FadeInOut ) );
edgeFade = smoothstep(0,1,edgeFade);

// multiply together
col *= edgeFade;
_Ramp is passed in from script to control the swoosh travel progression.
_Tiling is set in the material and allows for control of the length of the swoosh and how _Ramp effects the swoosh travel.
_FadeInOut is lets you set how mush on the ends of the swoosh to fade out.

A material property block can be used to send the information right to the swoosh renderer without messing with the materials at all like so:
MaterialPropertyBlock MPB = new MaterialPropertyBlock ();
Renderer thisRenderer = this.GetComponent ();

if (targetScreen) {
 MPB.SetFloat ("_ScreenSquish", 1.0f);
 MPB.SetVector ("_Divergence", new Vector2 (Random.Range (-screenDivergence, screenDivergence), Random.Range (-screenDivergence, screenDivergence)));
}

MPB.SetFloat("_Ramp", ramp);

thisRenderer.SetPropertyBlock (MPB);
Now a swoosh can be spawned anywhere, its script will drive _Ramp over a specified time, you know exactly when it will reach the end and what it will look like along the way.
Even when moving the camera a screen swoosh will always start at its world position and end at the screen position, the shape is always smooth and movement always fluid.
Lucky's tail swipe uses the same shader with an extra texture overlay to make it look more wispy. The tail swoosh is spawned at Lucky's position and rotation and then the end position is just updated to be Lucky's current position.
If Lucky is jumping, the tail swoosh it will follow him in the air while maintaining its smooth shape. It's a subtle effect but helps tie it to Lucky.
But we're not done yet. You can get really fancy with a geometry shader by turning each triangle into a little particle. Here one of the swoosh ribbons has the swoosh particle shader on it. This shader turned each triangle into its own quad and gave it some movement over its lifetime. Because this is just a shader you can play the whole effect backwards. And like the regular swooshes, the particle swooshes update their positions when the target moves.
How exactly that all works may be a post for another day though.

1 comment: