Black and white 1 style overlapping influence border

I have been thinking about this stupid problem for years now and was struggling a lot to wrap my head around it. I have a generated mesh that acts as a border and I’m generally pretty happy with the effect but one thing I was dealing with was how to prevent it from overlapping. I looked up all sorts of shader related weirdness to see if I could deal with the problem that way but the problem is a lot of the solutions were either 2D, ridiculously complicated metaball solutions that didn’t fit the comparatively simple mesh that was being generated.

Then I had a thought, one thing I do have to my advantage is that with the way this influence ring is generated is we know where the vertices are going to be placed before hand since it’s all generated through code. So shouldn’t it be possible to detect which vertices are overlapping and then delete them? I realise I’m asking for something potentially complicated to be explained to me but I’ve been working on this damn problem for about 3 or so years now and I’m not stopping until I solve it lol. I’ve been able to solve most of the issues I’ve been dealing with myself aside from a few specific maths problems which people here have been a great help on for my game. I’m just constantly having to wrap my head around this damn influence border.

Bgolus helped me out awhile back with the initial mesh generation and it works great but I’d love to refine the effect finally.

For the record, the influence rings have sphere collider triggers on them and I’ve been using them for a lot of my mechanics. I’m wondering if it’s possible that I could check which vertices are within a collider and then delete them to prevent the overlapping problem.

If it’s all circles, it’s as simple as checking if the distance between the XZ center of a given circle and the vertex’s XZ world position is less than the radius of the circle being tested against.

How would you delete the specific vertices that fail this check though? I think that’s my main question in this case, I’m pretty new to mesh generation in runtime.

You can update the vertex/triangle arrays at runtime freely. You could re-generate the arrays every frame, testing against all your rings to decide whether or not certain verts should not be generated. Or you could do that at key moments, depending on the logic of your game and what’s actually needed. You could even split your mesh into a bunch of quads and then you can do individual testing.

Keep in mind that discarding the verts is not a perfect solution. When you remove the verts, you’ll create an empty space where there originally was a segment. There’s a chance that this gap will coincide perfectly with where the other ring begins, but more often than not, you’ll end up with tiny gaps between your rings. The lower the resolution of the rings, the larger the gaps. If that’s OK for you, then by all means, go for it!

If not, then to fill that gap, you’d need to come up with a method to find the intersection points between a line and a circle. Then, add new verts at that intersection point. And I’m sure you’d run into even more problems down the road that I am unable to foresee.

Your problem interested me and I wanted to give it a shot at a shader approach, for my own education.

If you’re interested, here’s what I ended up with :

https://www.youtube.com/watch?v=M9w2h5YobQ4

Basically I create a texture for each rings and I store the position of all the other rings into it, every frame. Then, my shader iterates over that texture to calculate the distance between the fragments and the rings. If that distance is less than the radius of the ring being tested, then the alpha is simply 0.

I’m using a 4x4 texture, meaning I can support up to 16 rings, but you could increase that, should you need it.
Rings mapping code

private void Awake()
{
    ringsMap = new Texture2D(4, 4, TextureFormat.RGBAFloat, false);
    meshRenderer.material.SetTexture("_RingsMap", ringsMap);
}

private void LateUpdate()
{
    MapRings();
}

private void MapRings()
{
    List<Color> pixels = new List<Color>();

    for (int i = 0; i < ringsMap.width * ringsMap.height; i++)
    {
        if (i < rings.Length && rings[i] != this)
        {
            Vector3 ringPosition = rings[i].transform.position;
            pixels.Add(new Vector4(ringPosition.x, ringPosition.z, rings[i].radius));
        }
        else
        {
            pixels.Add(Color.clear);
        }
    }

    ringsMap.SetPixels(pixels.ToArray());
    ringsMap.Apply();
}

Here’s the shader
Here’s the shader

screenshot of the graph

7361129--896663--graph.jpg

custom function code

Out = 1;
float2 texelSize = float2(1 / TexSize.x, 1 / TexSize.y);

for (int x = 0; x < TexSize.x; x++)
{
    for (int y = 0; y < TexSize.y; y++)
    {
        float2 uv = (float2(x,  y) + 0.5) * texelSize;
        float4 data = SAMPLE_TEXTURE2D(Tex, SS, uv);
        float xDist = pow(abs(data.x - Position.x), 2);
        float yDist = pow(abs(data.y - Position.y), 2);
        Out = (xDist + yDist < data.z * data.z) ? 0 : Out;
    }
}

screenshot of the custom function

7361129--896660--cf.png

5 Likes

Holy crap @ADNCG that’s just the effect I was looking for, I had been researching this problem for years and you just solved it within a day LOL. I’ll need to study all of this properly including the mesh generation code again, I had been asking all over the forum about this as well.

2 Likes

For the record, yes it’s mainly a visual effect, I can get away with having overlapping sphere colliders as they are just designed to indicate whether you are within the influence ring or not, my mind is blown. Going to have to experiment with this and see what happens but thank you for helping me out with this, you and @bgolus finally helped me solve this damn problem. I had wondered if deleting the vertices would be a solution but you came up with a much better shader implementation.

I wonder why there’s so little information on this kind of technique? By the way, how exactly have you made this 4x4 texture? Is it just a blank colour or something more sophisticated?

Generated at runtime like this

ringsMap = new Texture2D(4, 4, TextureFormat.RGBAFloat, false);

The texture format is important since you want to retain the precision regarding the position of the rings.

Essentially, the required data for each ring is packed into its own pixel.
You know how a color has a red, green, blue and an alpha channel? Well, instead of being data that represents a color, it’s data that represents a ring.

It maps like this
r => the x position of the ring
g => the z position of the ring
b => the radius of the ring
a => wasted - no data needed

The shader then reads every pixel, extracts the data and uses it to determine the distance from the fragment.

Does that make more sense?

1 Like

Sort of but this is a tricky concept for me I’ll admit, are these calculations being done based on the rotation/position of the camera but automatically due to it being a shader?

No, none of that sorcery!

When a mesh is rendered, it’s broken down into fragments, tiny pieces of the triangles representing the mesh. You can get the world position of a fragment with the “Position” node in shadergraph.

In the ideal world, we’d pass a vector3 array to the shader, containing the world position of the rings, and their radiuses. The shader would then compare the fragment’s position to every rings and determine if the fragment should be visible or not. In your case, if a fragment is inside of another ring, then no, it shouldn’t be visible.

However, Shadergraph does not support vector arrays, but it supports textures! So, instead of passing an array, we pass a texture that represents the array.

For instance, we have a ring at position (3, 0, 6) with a radius of 1 meter. We aren’t interested in the Y position for this use case. We could represent that ring in the texture this way :

Color color = new Color(3f, 6f, 1f);

// or

Color color = new Color(ring.position.x, ring.position.z, ring.radius);

// We ignore the alpha channel since we only need 3 channels for our data

We then assign this to the first pixel in our texture, and so on for every other rings.

This way, we have sent our data over to the shader, in the form of a texture.

1 Like

Wait, so to try and break this down, what you did was essentially trick the shader graph into reading vectors and implemented the distance check between the generated circles again within the shader and deleting the result to prevent overlapping? That’s actually insane, I would have never have figured this out myself in a million years.

I was talking about this with someone I know in discord and they showed me a visual of the equation that you’re implementing in the shader graph, let me know if this is right. Should also dramatically help others figuring all this out because it helped me.

It’s more like “hiding” the result rather than deleting, but yeah, that’s spot on! :slight_smile:

No, there’s no need for that. To know if a point is inside of a circle, all you have to know is if the distance between the point’s position and the center of the circle is less than the radius of the circle.

The fragment is just iterating over every circles and doing this distance check. It’s that stupid simple. No further step.

1 Like

Thanks again, this has been a huge help and I’m sure this will help out a lot of other people who have been trying to get this kind of iconic effect working properly, I had no idea there were so many steps. Think I’ve just about got a proper grasp of it now.

The real question is now why the hell haven’t the Unity staff implemented vectors for shader graph to make this all easier? lol.

1 Like

Glad to help, this was an educating experience for me as well. Good luck with your game :slight_smile:

It’s a good question and beyond my understanding for the time being :slight_smile:

1 Like

By “vectors” do you mean arrays? Because basically all shaders do is manipulate vectors.

Part of the reason why arrays aren’t supported is because doing for loops in a node graph is a huge nightmare and 90% of node graphs for materials / shaders just punt on loops or provide custom nodes that do the loop w/o needing it explicitly supported by the graph. The other reason is because Unity doesn’t support arrays as material properties. Never has, so they didn’t add support for it for Shader Graph either.

That doesn’t mean Unity shaders don’t support arrays, they do. You just can’t have them as serialized properties of the material.

To get around that you just add the array as a uniform in the shader code itself. For Shader Graph that requires using a Custom Function that points to an .hlsl file.

Alternatively, you can use a texture like @ADNCG suggested, which can actually have some minor advantages, like the ability to change the number of elements in the “array” by changing out the texture with one with a different resolution, and get the max number of elements from the texture’s size.

One note about this approach. Your mesh is not a perfect circle. It can’t be, because it’s a mesh. So there’ll always be a little bit of a visible overlap or a gap between overlapping rings.

2 Likes

Just in case people hadn’t messed with custom functions and I know I sure as hell haven’t, here’s the appropriate documentation to go through what @ADNCG has laid out in their post. Press the graph inspector tab at the top right to view the node settings in the screenshot and set everything up in the shader graph, you’ll need to drag the window and scale it up when it pops up.

https://docs.unity3d.com/Packages/com.unity.shadergraph@10.4/manual/Custom-Function-Node.html

1 Like

@ADNCG if you could explain to me how you set up your rings array and list that would be great, I’ve been doing some poking at the code and I managed to get it setup without any major errors. I think the problem might be the pixels list because the MapRings function doesn’t seem to be doing anything on my end despite updating every frame when I add the colliders to the rings array in runtime?

Thought it would be a good idea to show a screenshot of where I’m at because I suspect I’m misinterpreting the code somewhere again. I’ve already made a modification to make it so that the radius matches my generated border, I made the pixels list public just to have a look at what it was doing.

Had a derp moment after re-reading your post and realised I shouldn’t even be using the sphere colliders for the check because we’re running checks on the damn vertex fragments. Could still use some help though in understanding where I’m messing things up.

Right, so essentially all you need is a way to obtain the position of the center of all the rings and their radiuses.

I’m trying to make sense of your setup from the screenshot. From my understanding, you have a RingMapper class on all of your rings and that class maintains a reference to all the other rings? I think that’s a bit chaotic, having to maintain relationship with all the rings from every ring.

Perhaps have one class responsible for maintaining a reference to all the rings and have the rings query the information from that class, if you want to maintain the logic on the rings class. Or, have rings be nothing but data and that that other class iterate over the rings and perform the logic instead. Or you could have a static list of rings inside of the rings class, and have each instance responsible for adding and removing itself.

At the end of the day, those are implementation details. If you’re happy with your solution, disregard my advice.

I think I’d need to see your code, I’m having trouble understanding what the problem is from the screenshot. Could you post your entire RingsMapper class?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RingMapper : MonoBehaviour

{
    public Texture2D ringsMap;
    public MeshRenderer meshRenderer;
    public MeshGeneratedInfluenceBorder[] rings;
    public List<Color> pixels;
    public float radius;

    public void Awake()

    {
        ringsMap = new Texture2D(4, 4, TextureFormat.RGBAFloat, false);
        meshRenderer.material.SetTexture("_RingsMap", ringsMap);
    }

    private void Update()

    {
        MapRings();
        radius = GetComponent<MeshGeneratedInfluenceBorder>().radius;
    }

    private void MapRings()

    {
        pixels = new List<Color>();

        for (int i = 0; i < ringsMap.width * ringsMap.height; i++)

        {
            if (i < rings.Length && rings[i] != this)

            {
                Vector3 ringPosition = rings[i].transform.position;
                pixels.Add(new Vector4(ringPosition.x, ringPosition.z, rings[i].radius));
                Debug.Log(rings[i].radius);
            }

            else

            {
                pixels.Add(Color.clear);
            }
        }

        ringsMap.SetPixels(pixels.ToArray());
        ringsMap.Apply();
    }


}

I’m 99% sure my shader code and graph is correct, I’ve triple checked that, so it’s likely just how I’ve set up your code. I’m getting a general understanding of how it all works though which is nice I just need to know how to work it all with the prefabs now, it’s some really weird maths. I’m trying to get it all to work in runtime as my rings are added.

if (i < rings.Length && rings[i] != this)

Well, for one “rings != this” is invalid because your rings are “MeshGeneratedInfluenceBorder” type and you’re checking against “RingMapper” type.

The code above works for me because my rings list is of the same type than the class I’ve put this code in. Both are type of “Ring” so I can actually compare them. For you, you could compare gameobjects instead, since you have your MeshGeneratedInfluenceBorder and RingMapper components on the same GOs. Or your list could be of type RingMapper.

The reason behind that line is that the current ring should not be added to its own rings map because the shader would have no way to know that this ring is the current ring, and thus wouldn’t be able to treat it differently. It would simply consider it as another ring laid right on top of the current, with the same radius, and wouldn’t render properly.

Vector3 ringPosition = rings[i].transform.position;

I’m not sure how you setup your mesh generation for your rings but if the transform.position is not at the center of the ring, you should update the line above to reflect the actual center of the ring. If it is, you can disregard what I said.

Also, the radius in your code “RingMapper” class isn’t used at all. You’re assigning its value but never reading it. You’re reading the radius straight from the MeshGeneratedInfluenceBorder, since that’s the type of your list.

I think there might also be a problem on the shader side. With the problems above, it shouldn’t work properly but I think you should be able to see at least some kind of behaviour. Can you post your graph and the custom node as well?

And just in case, are you certain that the materials of your rings are actually using the shader?

edit : Here’s the script I used when testing. Add it to a few gameobject in a blank scene. Make sure you add your material to the meshrenderer that it’ll create and you should be able to test to see if your shader is actually the problem.

Just in case, that code isn’t good code. It’s working code I used for testing. Don’t use calls like FindObjectsOfType every frame.

Rings

using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class Ring : MonoBehaviour
{
    [SerializeField]
    private float radius = 2f;
    [SerializeField]
    private float height = 0.4f;
    [SerializeField]
    private int resolution = 32;
    private MeshRenderer meshRenderer;
    private Texture2D ringsMap;
    private Mesh mesh;

    private void Awake()
    {
        ringsMap = new Texture2D(4, 4, TextureFormat.RGBAFloat, false);
        meshRenderer = GetComponent<MeshRenderer>();
        meshRenderer.material.SetTexture("_RingsMap", ringsMap);
    }

    private void Start()
    {
        mesh = new Mesh();
        GetComponent<MeshFilter>().mesh = mesh;
        RedrawMesh();
    }

    private void RedrawMesh()
    {
        List<Vector3> verts = new List<Vector3>();
        List<int> triangles = new List<int>();

        for (int i = 0; i < resolution; i++)
        {
            float angle = i * Mathf.PI * 2f / resolution;

            Vector3 pos = new Vector3(Mathf.Cos(angle) * radius, 0f, Mathf.Sin(angle) * radius);

            verts.Add(pos);
            verts.Add(pos + Vector3.up * height);
        }

        for (int i = 0; i < resolution; i++)
        {
            int index = i * 2;
            triangles.Add(index);
            triangles.Add(index + 3);
            triangles.Add(index + 2);

            triangles.Add(index + 1);
            triangles.Add(index + 3);
            triangles.Add(index);
        }

        for (int i = triangles.Count - 6; i < triangles.Count; i++)
        {
            triangles[i] %= verts.Count;
        }

        mesh.SetVertices(verts);
        mesh.SetTriangles(triangles, 0);
    }

    private void LateUpdate()
    {
        RedrawMesh();
        MapRings();
    }

    private void MapRings()
    {
        List<Color> pixels = new List<Color>();

        Ring[] rings = FindObjectsOfType<Ring>();

        for (int i = 0; i < ringsMap.width * ringsMap.height; i++)
        {
            if (i < rings.Length && rings[i] != this)
            {
                Vector3 ringPosition = rings[i].transform.position;
                pixels.Add(new Color(ringPosition.x, ringPosition.z, rings[i].radius));
            }
            else
            {
                pixels.Add(Color.clear);
            }
        }

        ringsMap.SetPixels(pixels.ToArray());
        ringsMap.Apply();
    }
}