Trying to render multiple circles of individual X,Y,Radius in Shader Graph

The intention is to have multiple fire zones to overlap into a greyscale mask to apply to a fire toon shader.

Right now, I am using a RenderTexture to blit a circle texture into itself as many times as I need it.
This is super costly and generates garbage in the memory, but the main issue is that the result looks fuzzy!

So I thought to use Shader Graph to draw Elipse of equal width/height and have them all MIN in order to combine them, but I can’t work out how to use LOOPS or how to pass a variable amount of information based on how many circles I have at one point + the X,Y,Radius for each of them.

I thought to use a lookup texture to pass inside of the shader and extract the data from RGB but the x,y can only be 256 positions and I need floats.

As a final note, the material will render a 4000/4000 quad which is my mesh which is why the end result is blurry (the RenderTexture of 2k/2k is too blurry still).

Thank you kindly for any help!

Just use a Custom Function node and then you can write the loop code inside of it normally instead of fussing with nodes for that part, it’ll be much cleaner. You can reference the array of X/Y positions assigned to the shader from in this code.

A texture can be many different types of formats. Not only 8bit per color (thought technically that would be 256*256 positions on the X/Y…) (Also keep in mind, the GPU will still read these values as 0.0-1.0)
For example you could have the texture be RGBAFloat and thus have full 32bit floating point values for each color channel, resulting in crazy precision. Though more ideally for something like this an RGHalf would be best, since it’s only 2 color channels (so just X/Y) in 16bit precision.

Instead of using a circle texture you could just do this analytically, so that sharpness is near-infinite and fully in your control. You would merely have your float3 array of circle positions, and the .z value would be the radius. And for each pixel you can loop through the array and compare that pixel’s distance from the circle position and then check against circle radius to get your value to min() with.

But this can become a bad route the more circles you need to draw at once, as the processing time for rendering all the pixels goes up linearly with each circle. If you intend to draw many circles then rendering them as quads onto the RT is generally a better approach since only the relevant pixels under a given quad are being processed.

There are many other approaches to this problem though if you look up “water ripple” shaders, it’s a very common graphics problem to find resources about.

If you’re rendering up to a few dozen circles, you can get away with the purely shader based approach. Beyond that and this is a dead end. Because of the way shaders work, every single pixel rendered of that shader is calculating every circle, every frame. If you need a few hundred circles you’re going to start bringing your computer to its knees trying to calculate them all constantly.

Really the correct way to go about this is with the way you’re already doing it. The parts that are expensive are more related to how you’re going about doing the rendering in the script than it actually being expensive. You probably want to look into using instanced rendering to manually render a large & variable number of quads in the locations you need. And for the resolution problems the trick there is to use a floating view of those quads. Don’t just render the full bounds all of the time if you can only see a small area of it.

There are a number of snow / grass displacement or tutorials out there that just spawn particles from an emitter that follows the player / moving objects and get rendered by a camera that’s above the player to a render texture. That texture is then used by the snow / grass shader to do the displacement. I would suggest you try to do something like that.

I need that texture to A) act as a mask for a simple toon fire shader and B) feed it into a particle system to spawn embers on the Y axis but, you know, only in the white circle spots.

I am using a Quadtree to generate a mesh for my world. The large shader for this bliting covers it all. Splitting it into pieces is very doable but the circles can be realtime meteorites falling in random places, leaving maybe-overlaping circles that fade into nothing over time, so the mesh would need re-splitting in realtime.

I have come across that particle snow thingy and ignored it because I already use my system for the snow purpose (that’s how i made it in the first place) but if it’s more efficient with large quantities of stuff, I can replace it with that!

Thanks for the RGBAFloat tip, I didn’t know this stuff exists! I actually need 4 values, X,Y,Radius and feathering (makes the edge a gradient to nothing). I guess I need a RGBAFloatHalf.

That’s what I need, the near-infinite edge instead of blurriness. The math involved for each circle is spot-on, I just never touched hlslvs before :confused:

I may have hundreds of circles of variable sizes, but itsn’t that JUST hundreds of distance+min() ?

“quads onto the RT is generally a better approach since only the relevant pixels under a given quad are being processed.” I don’t understand this part.

The problem is reading from your position texture 100 times.

I have come across this that might be the choice:
https://docs.unity3d.com/ScriptReference/Material.SetFloatArray.html

It is “just” that, yes. That’s at least 7 instructions by itself (sub, dot, sqrt, rcp, mul, min, loop), which isn’t too much. But doing that 100 times in a loop means a shader that’s running at least 700 instructions per pixel. “Just” for 100 circles. That’s a quite expensive pixel shader, especially if you’re rendering it to a 4k x 4k render target. And in reality it’s more than 7 instructions per loop, I’m being very conservative with my math here.

And that’s not counting the data access costs.

Sampling a texture 100 times per pixel is brutally expensive.

On some platforms accessing data from an array, or structured buffer, can be faster than sampling a texture. It’s still reading data 100 times per pixel which is expensive no matter how the data is getting to the shader.

If you use a shader to render all of the circles, reading from an array or texture to get all of the positions, every single pixel has to calculate every single circle.

Lets say you’re rendering to a 1000x1000 pixel render texture, and you need 100 circles.

Using the shader based approach means the GPU has to calculate the value for 1,000,000 pixels, each one being >700 instructions, and accessing data 100 times. That means the GPU is running 700 million fragment shader instructions and access a texture / array 100 million times.

Using the “render a bunch of quads” based approach it’ll depend on how big the circles are, but lets say they’re all 20x20 pixels and they’re all visible. Lets also say they’re instanced so each quad mesh is sampling an array to get it’s position for the 4 vertices. Now that’s only having to run 280,000 fragment shader instructions, and only having to read from the array 400 times. There’s some additional overhead for setting up the instancing, but even if the circles are all 500x500 pixel it still ends up being cheaper.

Thank you for the comprehensive explanation. I am scared right and I understand more how shaders work. I thought the effort was only in the circle pixels but now I understand the effort is EVERYWHERE ALL THE TIME.

Taking this approach out of the discussion, what would you recommend as a solution?

I looked into the snow example where an orthographic camera renders particles to a RenderTexture which is similar to my manual Graphics.Blit loop sandwitch. The issue remains that it has to dump information into a texture and the blur remains :confused:

When rendering to the texture using the quads, you can still use an analytical approach, instead of sampling a circle texture that has finite resolution you calculate one using the UV value of the quad, checking the distance of the UV value from the center UV value (you can pre-configure the UV of the quad mesh data to be offset -0.5 on both axis so you have a corner-relative UV value that can just be run through an abs()*2 and will know center is always 0,0). (or pre-mul the UV by 2 as well in the mesh data to avoid that multiply instruction in the shader)

Since you’re only using this as a mask, you can also make better use of your RT by outputting your circle values as an SDF, so when the RT is later sampled, you can control sharpness based on the gradient sampled.

Just to be sure I understand everything:

  1. Rendering using the Camera/RenderTexture is STILL better than using Graphics.Blit loop in Update?
  2. Instead of spawning particles, spawn the required quads / resize / position them and let Camera render to a Render Texture.
  3. Instead of a Shader Graph to draw the circles, use SDF shaders? (I need to research this from scratch).
  1. Yes definitely.
  2. You’re describing particles there. The important part is to be using instanced rendering (and thus ensuring your shader is supporting instancing) so that they can be rendered as a single draw call. (You don’t want to be instantiating hundreds of quad GameObjects each frame, just rendering them)
  3. SDF is just a way of representing and calculating data. You can still use Shader Graph, you would just be doing a little extra math when sampling the RT.

For 2) Is this “Enable GPU Instancing” setting on the circle Material?

For 3) it is pretty trivial to draw a filled circle, and I don’t care about Zfighting because the white parts will just overlap and simulate Math.MIN. I want to add feathering to the edges (also easy) so when an effect ends, it’s not a shrinking circle, but it smears down towards the center.

Yes. Though as a side comment, you don’t actually need a camera to render to a render texture. You can render directly to a render texture, which is all Blit() is doing. It does however make some aspects a little easier as it means the projection and view transform matrices are all handled for you.

Or actually use a particle system. Shader Graph doesn’t (yet) support instancing, so you can’t write an instanced shader with it. But you can also easily render a few hundred quads using a particle system since, as @Invertex mentioned, that’s all particle systems are. You can directly control the position and lifetime of particles by manually setting them with particleSystem.SetParticles().
https://docs.unity3d.com/ScriptReference/ParticleSystem.GetParticles.html
I’m linking the GetParticles() documentation since it actually has a (limited) code example.

Instanced quads would probably be better, but you’d have to write a shader by hand for that. Because, again, Shader Graph doesn’t support it.
https://toqoz.fyi/thousands-of-meshes.html

I plan to use DrawMeshInstanced to draw props like trees & whatnot, since they won’t interact with anything and don’t need to be GameObjects.

You saying I can use that as well so I don’t have to instantiate anything? Neato!
For the small concept of a circle with feathering, I am positive I can find an example online.

Thanks for all your help !!!