9
\$\begingroup\$

Was watching some Rocket League and notice there was animated decals and wheels.

Image.

I would like to implement something similar to the effects in the image above.

How would I go about writing a Unity Shader to do the wheel effect?

I don't know much about Shaders, but can I edit the standard Unity Shader to do the animation effect?

\$\endgroup\$
  • 3
    \$\begingroup\$ Shaders are definitely the go-to solution here, even for something as simple as a scrolling texture. Unity has a built-in _Time variable you can add to your texture coordinates in the (vertex) shader to get a scrolling effect dirt cheap. The hexagon effect would be pretty straightforward too. If you edit your question to highlight just one effect and ask "how would I implement this in a Unity shader," we can probably help you out. Be sure to specify whether the shader needs to respond to lighting/shadowing, as that has impacts on how it would be written. \$\endgroup\$ – DMGregory Jan 6 '17 at 0:07
  • \$\begingroup\$ Thanks for the info. Have changed my question, is that a little better? I don't really know about the lighting and shadowing, so I left that out for now until I get a better understanding of Shaders. I guess I would want them, but I could just copy parts from the standard shader right? \$\endgroup\$ – JacketPotatoeFan Jan 6 '17 at 0:35
  • 3
    \$\begingroup\$ I'll expand on it in an answer a little later tonight (though feel free to beat me to it, if anyone wants to answer now!) - but you'd typically write a Unity shader that uses lighting as a "Surface Shader" while one that doesn't need lighting is often simpler to write as an "Unlit Shader". It looks to me like those wheels are unlit (the slight shadowing at the edge looks like SSAO), so I'd leave lighting out of the picture for starters. Can you clarify: do you want exactly the geometric pattern shown here, or the ability to scroll arbitrary textures outward & rainbow-tint them like that? \$\endgroup\$ – DMGregory Jan 6 '17 at 0:39
  • \$\begingroup\$ Thanks so much, whatever info you can give to get me started would be great. I really admire people who can write shaders, I''ve looked over a few articles for Unity Shaders, and yeah, very confusing. I think simply scrolling through textures (or texture uvs?) would be good enough, and with the ability to tint. \$\endgroup\$ – JacketPotatoeFan Jan 6 '17 at 0:44
12
\$\begingroup\$

I'll build this up in a few layers so you can see how it comes together.

Start by creating a new shader in Unity by choosing Create --> Shader --> Unlit in either the Assets menu or the right-click context menu in the Project window.

In the top block we'll add a _ScrollSpeeds parameter to control how fast the texture moves in each direction:

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _ScrollSpeeds ("Scroll Speeds", vector) = (-5, -20, 0, 0)
}

This exposes a new 4-component float property in the material inspector, with the friendly name "Scroll Speeds" (similar to adding a public or Serialized variable to a MonoBehaviour script)

Next we'll use this variable in the Vertex shader to shift the texture coordinates (o.uv), by adding just two lines to the default shader:

sampler2D _MainTex;
float4 _MainTex_ST;
// Declare our new parameter here so it's visible to the CG shader
float4 _ScrollSpeeds;

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);

    // Shift the uvs over time.
    o.uv += _ScrollSpeeds * _Time.x;

    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

Slap that on a quad (with a lovely free giraffe texture by Kenney) and you get:

A quad with a repeating giraffe scrolling across it.

To get the texture scrolling outwards in a ring, we could just use a mesh subdivided like a spiderweb, with the uv v coordinate increasing from the center out. But that will give some sawblade-shaped artifacts on its own. Instead, I'll show how we can calculate our UVs per fragment.

This is a bit more costly, both because of the trig & length operations (which are more expensive than basic math) and because it's not as efficient to predict & cache texture data in the hardware when calculating texcoords per fragment, compared to just interpolating them between vertices. But for a little special effect like this, it's not excessive.

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);

    // Shift the UVs so (0, 0) is in the middle of the quad.
    o.uv = v.uv - 0.5f;

    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    // Convert our texture coordinates to polar form:
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x)/(2.0f * 3.141592653589f), // angle
           length(i.uv)                                    // radius
        );

    // Apply texture scale
    polar *= _MainTex_ST.xy;

    // Scroll the texture over time.
    polar += _ScrollSpeeds.xy * _Time.x;

    // Sample using the polar coordinates, instead of the original uvs.
    // Here I multiply by MainTex
    fixed4 col = tex2D(_MainTex, polar);

    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

That gives us something like this (here I increased the tiling parameters in the material so it's clearer what's happening - wrapping only a single repeat of the tile around the circle looks distorted and weird)

Tiling giraffes radiating outward from the center of a quad

Lastly, to tint the texture by a scrolling gradient, we can just add the gradient as a second texture and multiply them together.

First we add the new texture parameter at the top:

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _TintTex("Tint Texture", 2D) = "white" {}
    _ScrollSpeeds ("Scroll Speeds", vector) = (-5.0, -20.0, 0, 0)
}

And declare it in our CGPROGRAM block so the CG shader can see it:

sampler2D _MainTex;
float4 _MainTex_ST;

// Declare our second texture sampler and its Scale/Translate values
sampler2D _TintTex;
float4 _TintTex_ST;

float4 _ScrollSpeeds;

Then update our fragment shader to use both textures:

fixed4 frag(v2f i) : SV_Target
{
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
           length(i.uv)                                    // radius
        );

    // Copy the polar coordinates before we scale & shift them,
    // so we can scale & shift the tint texture independently.
    float2 tintUVs = polar * _TintTex_ST.xy;
    tintUVs += _ScrollSpeeds.zw * _Time.x;

    polar *= _MainTex_ST.xy;
    polar += _ScrollSpeeds.xy * _Time.x;

    fixed4 col = tex2D(_MainTex, polar);
    // Tint the colour by our second texture.
    col *= tex2D(_TintTex, tintUVs);

    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}

And now our giraffe gets really trippy:

far out, man....

With a slightly more artistic selection of textures and scrolling rates, this can create an effect quite similar to the one shown in the question.


You may notice two small artifacts with the version I've shown above:

  • The faces near the center of the circle get a stretched/skinny/pointy, then as they move toward the outside they get squashed/wide.

    This distortion happens because we have a fixed number of faces around the perimeter, but the circumference they're spanning gets wider as the radius increases, while their height stays the same.

    We can fix this by remapping the vertical component of the texture sample to follow a logarithmic curve, so repeats of the texture are further apart as the radius increases, and closer together toward the center. (In fact,this gives us an infinite regress of smaller and smaller giraffes...)

  • There's a row of one or two blurry pixels along the middle-left of the quad.

    This happens because the GPU looks at two adjacent texture sample coordinates to figure out what filtering to use. When the samples are close together, it figures the texture is being displayed large/close and shows the most detailed mip level. When the samples are far apart, it guesses we must be showing the texture at a tiny zoom or far away, and it samples from a smaller/blurrier mipmap to ensure we don't get sparkly aliasing artifacts.

    The trouble is here, we're at the wrap-around point in polar coordinates, from -180 to 180 degrees. So we're actually sampling from very similar points in our repeating texture space, even if their numeric coordinates make them look like they're far apart. So we can provide our own sampling gradient vectors to correct for this.

Here's a version with these corrections:

fixed4 frag(v2f i) : SV_Target
{
    float2 polar = float2(
           atan2(i.uv.y, i.uv.x) / (2.0f * 3.141592653589f), // angle
           log(dot(i.uv, i.uv)) * 0.5f                       // log-radius
        );

    // Check how much our texture sampling point changes between
    // neighbouring pixels to the sides (ddx) and above/below (ddy)
    float4 gradient = float4(ddx(polar), ddy(polar));

    // If our angle wraps around between adjacent samples,
    // discard one full rotation from its value and keep the fraction.
    gradient.xz = frac(gradient.xz + 1.5f) - 0.5f;

    // Copy the polar coordinates before we scale & shift them,
    // so we can scale & shift the tint texture independently.
    float2 tintUVs = polar * _TintTex_ST.xy;
    tintUVs += _ScrollSpeeds.zw * _Time.x;

    polar *= _MainTex_ST.xy;
    polar += _ScrollSpeeds.xy * _Time.x;

    // Sample with our custom gradients.
    fixed4 col = tex2Dgrad(_MainTex, polar, 
                           _MainTex_ST.xy * gradient.xy,
                           _MainTex_ST.xy * gradient.zw
                         );

    // Since our tint texture has its own scale,
    // its gradients also need to be scaled to match.
    col *= tex2Dgrad(_TintTex, tintUVs,
                          _TintTex_ST.xy * gradient.xy,
                          _TintTex_ST.xy * gradient.zw
                         );

    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}
\$\endgroup\$
  • 2
    \$\begingroup\$ A couple of small improvements one can make: 1) using a logarithmic curve for the v direction helps the texture retain its shape as it scrolls outward (instead of narrowing to a point in the middle, you just get an infinite chain of smaller copies until the mip smears it out) 2) calculating your own texture gradients and using tex2Dgrad fixes the filtering artifact where the angle wraps around from -pi to +pi (that's the thin line of blurred pixels on the left side). Here's the touched-up result with more blue \$\endgroup\$ – DMGregory Jan 6 '17 at 4:45
  • \$\begingroup\$ Amazing, thanks for the break down as well. I feel Shaders are something I may never be able to write (especially stuff like what you have done for the "polar", the math stuff just means nothing to me, as I've only got very basic math skills). \$\endgroup\$ – JacketPotatoeFan Jan 6 '17 at 18:23
  • 1
    \$\begingroup\$ Stuff like that folks would usually just look up. ;) I just do it enough that it rolls off the keyboard. Try playing around with different math functions though and look at the results — having a tool like this to help me visualise what the math was doing geometrically is how I got my practice in the first place. (Not with shaders specifically, but an old WinAmp visualiser called WhiteCap...) Even when shader math goes horribly wrong it's often strangely cool to look at. :D \$\endgroup\$ – DMGregory Jan 6 '17 at 18:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.