Let’s go on a little technical odyssey today, starting with some shader math and then wind our way through a few creative chasms of problem-solving. I'm going to get into some gritty details, but I think that the holistic thought process and lessons can translate to many endeavors. Let's go.
Just Gusts
The high level problem that kicked this off is FX visuals for Gust Funnels - corridors of wind flowing over structures, breathing energy into them, visible not just in UI but in-world. I'll do a quick high level of a fun technique I used to get the corridor effect itself.
Always UV Manipulation. To simulate a cylinder of air, we can run a sin wave through the V coordinate and then add that result to our U value which creates an apparent semi-circle perspective bend akin to the front view of a cylinder. We can then duplicate that and subtract instead giving us a full transparent wind corridor. This also gives us a nice added benefit of having a mask to multiply by to make sure the edges of the effect are soft and natural feeling.

That Was the Easy Part
Ok so some fellow nerd out there is assuredly going to tell me a better way to do this (and please do so in the comments below), but I had a seemingly simple problem to solve that turned out to be not so simple. I just needed to pick a random UV starting location for each Gust because when they all sync up it's very noticeable, and you are easily going to see dozens of these at once. No self-respecting FX Artist would allow such a travesty to happen.
Usually to solve this problem I use the exposed in-world object position in the Shader to do some sort of picking, this is why Mana Flares and trees all have their unique properties. And here's where the trial and error begins:
- Line Renderers ignore world position entirely - they all report 0,0,0. Useless.
- World space bounds would almost solve this problem, but this quickly runs into floating point errors where as your character moves, a line will just change it's value and the effect will pop. No good.
- World-space UV Interference? Almost, but horizontal and diagonal lines distort very differently.
- We could pass in a Material Property Block, but those tend to break batching more often than not, and also introduce a performance overhead that I'm not comfortable with in a perf-sensitive game.
No dice from the usual suspects. What now?
Let's Get Creative
One thing that every object natively has is Vertex Color. This has a ton of utility, because as mentioned last week, full RGBA channels is a lot of freedom. The Entropy shader uses this to great effect where I literally intercept the R and G channel and use that for UVs while B and A are Color and Alpha. So that's obviously the solution right? Almost... the trouble is Unity automatically multiplies any Sprite Lit shader by RGBA at the end even if you call it explicitly inside the shader. So no-go?
Well, there is one trick left. What if we made extra space in the RGB information from (almost) nothing? Human perceptibility, especially in the top range of whites is notoriously noisy, so what if we pick a random value in just the top 3% of brightness, across all 3 RGBs and used that to generate a full random range? In Hex code terms that would be about #F7F7F7 to #FFFFFF that would give us plenty of possible starting locations for UV's.

But We're Not There Yet
Somehow, we need to cast a full range of Vector 3 (RGB) into a full range of Vector 2 (UV). And I don't actually know the best way to do that. So back to brainstorming, for simplicity let's imagine a 4x4 grid:
- We could definitely use R and G to map straight to U and V, but that leaves out an entire axis of options. 16 unique choices is significantly less than 64, and that disparity only grows as the number of samples grow.
- We could make some giant random number from the three using primes or dot products, and then you can either Modulo that or throw it into a sin wave to clamp arbitrarily large numbers down. The issue with Mod is that as the numbers get larger and larger you start losing fidelity and its hard to get an accurate distributed sample. Sin has similar issues where there is inordinate clustering near 0 and 1 and we'd prefer to avoid that.
- Maybe we can use B to multiply the R and G coordinates, that's directionally good, but we'll get clustering in the first quadrant and scarcity in the last.
- What about an additive offset? We could slide the points within their local cluster, but that is still going to create predictive gaps in the field.

We're certainly getting close, and now the exercise is revealing itself to be 'How can I get a single number to distributively traverse a 2d area?' But also be within the confines of shader logic.
The Final Trick?
Seriously, how many tricks can you put up one sleeve? ...At least one more.
So let's be clear - this problem is super fun academically, but I still want to try and solve it in a timely manner (like a day or less) and at bottom all we are trying to do is get a random starting UV offset. And I can't find a good way to get 3 axis into 2. But I do know how to get 6 into 2...

There's this cool thought experiment where PI contains every other number. Pi is not just 3.1415, it's also 1.4159, and so on. Any time you generate a random number, you secretly get at least one extra random number with a little help. In the case of a roll 0-1 Simply multiplying your result by 10 and taking the remainder gives you a full extra 'random' float to work with. There are nuanced problems with this, but for our simple concerns, they work perfectly.
Now our RGB values become R1, R2, G1, G2, B1, B2. From there, U = R1 + R2 + G1; V = G2 + B1 + B2.
Six into two. All three values contributing a distributed set of results to our UVs.

above: random UV starting positions | below: same UV starting positions
Pocket Dimension Recap
So because of the limitations of the engine and time frame we are working in, we have pulled a random roll out of the top range of imperceptibility from our Vertex Color, and then for want of easier math to work with, we generated a new set of random numbers from those rolls by lopping off the first digit and using the rest as a new number.
I am certain there are better ways to do this, but I am also certain that at some point an opportunity will present itself for you to either use one of these hacks directly, or find your own way to manufacture your own degrees of freedom, now that you know it is a potential tool in your kit. Every system has limits, but creativity begins where the math runs out.
Back to the lab 🌿
~CyanAvatar
