Outline of combined cicles

I’m using cicles to represent detection range in my game, is there a relativly simple way to combine the cirles and only draw the outline?

Right now I’m just drawing lines to make a circle shape.

private void DrawCircle(Vector2 center, int radius, Color color, SpriteBatch spriteBatch)
{
int numSegments = 15;

        float angleIncrement = MathHelper.TwoPi / numSegments;

        for (int i = 0; i < numSegments; i++)
        {
            float angle = i * angleIncrement;
            Vector2 startPoint = center + new Vector2((float)Math.Cos(angle) * radius, (float)Math.Sin(angle) * radius);
            angle = (i + 1) * angleIncrement;
            Vector2 endPoint = center + new Vector2((float)Math.Cos(angle) * radius, (float)Math.Sin(angle) * radius);

            Game1.mainGame.DrawLine(Game1.mainGame.pixel, 1, color, startPoint, endPoint);
        }
    }


I would like just the part where I have drawn a red line to show.

hm… Since you are using lines, maybe just check collision… Only lines that are outside all circles are drawn in red instead of green… Sure you have to check a lot of little collisions, but you can do thousands of such checks no problem…

Also, you can just do this when a new radar is introduced, or they move… So you just set the border when it NEEDS to change.

One way to do it is with Stencil.
Draw all the filled cicles first with a state that will increase the stencil count.
Then draw the the borders with a stencil test to pass only when the stencil count is 1.

You could do the same masking with rendertargets , shaders and/or states.

2 Likes

That seems logical, but how do I know from which vector to draw from and to? When they are circles, I just follow around in the order they are made. The order is the importent part then.

I could just remove all vectors that is inside the range of a “radar”, that is easy. Perhaps I could make it draw to the closest one that have not been used before, but then I would have to check distance and that is a bit slow, then again I would only have to do it when the radar needs to update as you say.

a little opinion first:
Remember a particle system can control hundreds of particles in real time, that have quite advanced logic, like collision detection, so you don’t need to be so scared of thousands of things per frame.
-You could probably draw single pixels on the perimeter of each circle, and set the color from how many radar-circles they collide with… Not arguing for it, but a modern machine could, and I think that would be a valid approach.

for lines though, the trick is intersections… Where circles intersect, a line segment is inside and outside a circle, so what color to draw that?

Imagine, like now you have a list of points that you are drawing from, you could have an additional list of points to highlight… Containing a select few, from the master list… And you just draw those instead of or on top of the others… But you will get some deformation near the intersections.

Another aproach might be to adjust the point spacing / line length, dynamically, so that you never get lines that intersect… They END where they meet, instead of crossing.

… I imagine something like a normal 360 circle, intersects with another, at 2 points.
You get the angle between these 2 points, and subtract it from 360.

Then that number you split EVENLY between some whole number of points…

So you’d get nice evenly spaced line segments, that never intersect… But this is if you want that vector graphics look… If you really want a smooth circle, you could be doing that instead.

In this particular case, I would recommend the approach suggested by nkast. Draw all the filled-circles on top of each other to get an ‘inside’ stencil, then draw all the same circles a pixel or two bigger and don’t draw any pixels that are within the ‘inside’.

Since the concept of what you’re displaying (‘detection range’) is an area rather than an outline, you’re reasonably likely to discover at some point later on that you want to ‘fill in’ that area in some manner, instead of merely illustrating it’s perimeter. At that point a line-based approach won’t help, but with an area-based approach most of the work would already be done.

4 Likes

One way to do it is with Stencil.
Draw all the filled cicles first with a state that will increase the stencil count.
Then draw the the borders with a stencil test to pass only when the stencil count is 1.

You could do the same masking with rendertargets , shaders and/or states.

This is correct answer, Mono’s suggestion is borderline insanity, sorry.

2 Likes

I think it depends, my suggestion is based on the fact, that he is already drawing shapes from a list of points… If he had wanted to draw big circle sprites / stencils instead, I’m presuming he would have started with that.

-for what the rest of us are imagining from our own projects, I would use stencils too…
-I just don’t think that’s what this is?

OK, I thought this was an interesting problem and decided to have a quick go at it. I have a solution, but if I am honest, it’s not the best.

I am using line primitives to do this, it would probably be better to do this in a shader, and I might even give it a go another time. I think the principle will be the same though “Only draw the circle where it does not intersect another circle”

Anyway, hope this experiment helps :slight_smile:

So, here is where I am drawing my circles

This is how it looks when I run my exclusion code on it (rendering the circle in white, and where it intersects another circle it’s rendered transparent)

If I run them at the same time, you can see the intended circles (in black) and the outline (in white)

As I say, I have done this pretty quick and dirty, but it does the job.

Here is my code.

The Draw call looks like this:-

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);

            List<Vector3> circles = new List<Vector3>()
            {
                new Vector3(0,0,.5f),
                new Vector3(.5f,1f,1),
                new Vector3(-.5f,.5f,.5f),
                new Vector3(-1f,-.25f,.5f),
                new Vector3(-1f,.5f,.5f)
            };

            foreach (Vector3 c in circles)
            {
                DrawCircle(c, Color.Black);
                var otherCircles = circles.Where(w => w != c).ToList();
                DrawCircle(c, Color.White, otherCircles);
            }
        }

So, I create a list of “circles” each one is a Vector3 with the X,Y being it’s position and the Z it’s radius. Naturally you won’t do this in your draw call, as I say, this is very quick and dirty.

I then loop through each one and draw it, for the outline, I pass it all the other circles, again, quick and dirty.

First call to DrawCircle draws the whole circle in black each time, the second draws the white outline.

DrawCircle, is a grubby old thing used to generate the line primitives for the circle to draw, the key element, the bit that does the draw outline only is this:-

// If the point in inside any of the other circles, don't draw it..
bool insideOtherCircle = circles != null && circles.Any(a => Vector3.Distance(p, new Vector3(a.X, a.Y, -dist)) < a.Z);

The function as a whole looks like this:

protected void DrawCircle(Vector3 circle, Color? color = null, List<Vector3> circles = null, int segments = 256) 
{
    if (color == null)
    {
        color = Color.Red;
    }

    List<VertexPositionColor> pointsList = new List<VertexPositionColor>();

    float dist = 5;

    List<short> inds = new List<short>();
    double step = MathHelper.TwoPi / segments;
    Vector3 center = new Vector3(circle.X, circle.Y, -dist);
    float radius = circle.Z;
    Vector3 p = Vector3.Zero;

    for (double angle = 0; angle < MathHelper.TwoPi; angle += step)
    {

        float x = radius * (float)Math.Cos(angle);
        float y = radius * (float)Math.Sin(angle);

        p = center + new Vector3(x, y, 0);

        // If the point in inside any of the other circles, don't draw it..
        bool insideOtherCircle = circles != null && circles.Any(a => Vector3.Distance(p, new Vector3(a.X, a.Y, -dist)) < a.Z);

        if (insideOtherCircle) // still record the segment, but make it transparent.
        {
            pointsList.Add(new VertexPositionColor(p, Color.Transparent));
            inds.Add((short)inds.Count);

            x = radius * (float)Math.Cos(angle + step);
            y = radius * (float)Math.Sin(angle + step);

            p = center + new Vector3(x, y, 0);
            pointsList.Add(new VertexPositionColor(p, Color.Transparent));
            inds.Add((short)inds.Count);
        }
        else
        {

            pointsList.Add(new VertexPositionColor(p, color.Value));
            inds.Add((short)inds.Count);

            x = radius * (float)Math.Cos(angle + step);
            y = radius * (float)Math.Sin(angle + step);

            p = center + new Vector3(x, y, 0);
            pointsList.Add(new VertexPositionColor(p, color.Value));
            inds.Add((short)inds.Count);
        }
    }

    DrawPoints(pointsList.ToArray(), inds.ToArray(), segments);
}

And just so you can draw the points for your circle, DrawPoints looks like this:

protected void DrawPoints(VertexPositionColor[] points, short[] index, int primatives)
{
    if (basicEffect == null)
        basicEffect = new BasicEffect(GraphicsDevice);

    GraphicsDevice.BlendState = BlendState.AlphaBlend;
    basicEffect.World = Matrix.Identity;
    basicEffect.View = Matrix.CreateScale(Vector3.One) *
              Matrix.CreateTranslation(new Vector3(0,0,0));
    basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.PresentationParameters.BackBufferWidth / (float)GraphicsDevice.PresentationParameters.BackBufferHeight, 0.1f, 1000);
    basicEffect.VertexColorEnabled = true;

    basicEffect.CurrentTechnique.Passes[0].Apply();
    GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.LineList, points, 0, points.Length, index, 0, primatives);
}

Hope this helps and has not added confusion to the topic.

Happy coding :slight_smile:

3 Likes

While I’m not sure how to achieve this with a stencil buffer, @Charles_Humphrey’s solution of examining the points of a circle is where my brain went. I was more thinking of doing a union of the vertices though, which kinda sent me down a bit of an internet rabbit hole.

It turns out there’s a lot of ways to do this, and a lot of existing libraries to look at. Here’s a repo with an implementation that might be helpful…

Also, see this post…

It’s got an interetsing approach and starts off with a good explanation of building out an understand of the probem, but they lose me in step 4 and I’m not quite sure what they’re doing.

I was also thinking about how I’d do this with a shader and I’m not sure how I’d do that either. If you wanted either transparent or not transparent it would be easy enough to check a list of circles to see if any given point was in one or not and set the colour accordingly, but I’m not sure how to only render the border of it. Which is probably maybe my same mental block for how to approach it with stencils too.

1 Like

I like the code, and it seems to run fast (Much faster than my try) but I don’t get it to draw anything. (The code runs) Sometimes I see flickering lines. I tried making a new project with only this code, and it doesn’t draw anything? Is there a spesific version or any other setup I would need?

Nope, nothing special in there or needs setting up…

I am at work at the min, ill see if I can create it here with the code I posted, see if I missed a trick. Failing that I’ll see if I can DM you the code, don’t really want to create a repo just for this.

OK, just created a cross platform project, and pasted the code over and it worked OK.

Here is the entire Game1.cs file

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CirclesTest
{
    public class Game1 : Game
    {
        private GraphicsDeviceManager _graphics;
        private SpriteBatch _spriteBatch;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;
        }

        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            base.Initialize();
        }

        protected override void LoadContent()
        {
            _spriteBatch = new SpriteBatch(GraphicsDevice);

            // TODO: use this.Content to load your game content here
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);

            List<Vector3> circles = new List<Vector3>()
            {
                new Vector3(0,0,.5f),
                new Vector3(.5f,1f,1),
                new Vector3(-.5f,.5f,.5f),
                new Vector3(-1f,-.25f,.5f),
                new Vector3(-1f,.5f,.5f)
            };

            foreach (Vector3 c in circles)
            {
                DrawCircle(c, Color.Black);
                var otherCircles = circles.Where(w => w != c).ToList();
                DrawCircle(c, Color.White, otherCircles);
            }
        }

        protected void DrawCircle(Vector3 circle, Color? color = null, List<Vector3> circles = null, int segments = 256)
        {
            if (color == null)
            {
                color = Color.Red;
            }

            List<VertexPositionColor> pointsList = new List<VertexPositionColor>();

            float dist = 5;

            List<short> inds = new List<short>();
            double step = MathHelper.TwoPi / segments;
            Vector3 center = new Vector3(circle.X, circle.Y, -dist);
            float radius = circle.Z;
            Vector3 p = Vector3.Zero;

            for (double angle = 0; angle < MathHelper.TwoPi; angle += step)
            {

                float x = radius * (float)Math.Cos(angle);
                float y = radius * (float)Math.Sin(angle);

                p = center + new Vector3(x, y, 0);

                // If the point in inside any of the other circles, don't draw it..
                bool insideOtherCircle = circles != null && circles.Any(a => Vector3.Distance(p, new Vector3(a.X, a.Y, -dist)) < a.Z);

                if (insideOtherCircle) // still record the segment, but make it transparent.
                {
                    pointsList.Add(new VertexPositionColor(p, Color.Transparent));
                    inds.Add((short)inds.Count);

                    x = radius * (float)Math.Cos(angle + step);
                    y = radius * (float)Math.Sin(angle + step);

                    p = center + new Vector3(x, y, 0);
                    pointsList.Add(new VertexPositionColor(p, Color.Transparent));
                    inds.Add((short)inds.Count);
                }
                else
                {

                    pointsList.Add(new VertexPositionColor(p, color.Value));
                    inds.Add((short)inds.Count);

                    x = radius * (float)Math.Cos(angle + step);
                    y = radius * (float)Math.Sin(angle + step);

                    p = center + new Vector3(x, y, 0);
                    pointsList.Add(new VertexPositionColor(p, color.Value));
                    inds.Add((short)inds.Count);
                }
            }

            DrawPoints(pointsList.ToArray(), inds.ToArray(), segments);
        }

        BasicEffect basicEffect;

        protected void DrawPoints(VertexPositionColor[] points, short[] index, int primatives)
        {
            if (basicEffect == null)
                basicEffect = new BasicEffect(GraphicsDevice);

            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            basicEffect.World = Matrix.Identity;
            basicEffect.View = Matrix.CreateScale(Vector3.One) *
                      Matrix.CreateTranslation(new Vector3(0, 0, 0));
            basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, GraphicsDevice.PresentationParameters.BackBufferWidth / (float)GraphicsDevice.PresentationParameters.BackBufferHeight, 0.1f, 1000);
            basicEffect.VertexColorEnabled = true;

            basicEffect.CurrentTechnique.Passes[0].Apply();
            GraphicsDevice.DrawUserIndexedPrimitives(PrimitiveType.LineList, points, 0, points.Length, index, 0, primatives);
        }
    }
}

Hope this helps.

1 Like

I see what I failed to see, I usually draw anything based on top left of the screen, but you example put it in the center. I’m quite embarrassed now. Thank you, it works great, and I can move on to the next problem.

1 Like

Nooo, dont be daft, we all go code blind from time to time. Glad you got it working.