To Spline or not to BSpline

posted in A Hobbyist Tale
Published January 15, 2025
Advertisement

During the last month or so, I've noticed a couple of impressive custom node editors here in the forums.
It's hard to resist not getting in on that. In this blog post, the focus will be on the link connectors.

Typically, what I saw was the sharp bent line. It makes perfect sense to use this style. Quick and low overhead.
Three segments with four vertices. Nice. Nothing wrong with that at all.
Then I started hunting for spline code examples until the math bits showed teeth with that angry growl.
If I have to pull out sqrt or some form of smoothstep just for pretty lines, then maybe it's not worth it.
So, what to do…what to do. Well, in my case, I took a nap. 🙂
After waking from slumber, I had the ‘just fake it’ idea. Why not, right?
Here, I'll just show the generate points part and call it good.

core::cLine _spline;
sPoint _controlPoints[5];

bool gui::cSpline::Create(sPoint start, sPoint end)
{
    _controlPoints[0] = start;
    _controlPoints[1] = sPoint(start.x + 4, start.y);
    _controlPoints[2] = sPoint(start.x + ((end.x-start.x)/2), start.y + ((end.y-start.y)/2));
    _controlPoints[3] = sPoint(end.x - 4, end.y);
    _controlPoints[4] = end;
    std::vector<sPoint> points = GenerateSegments();
    
    _spline.data.stride = 2;
    _spline.data.num_verts = points.size();
    for (sPoint pt : points)
    {
        _spline.data.vdata.push_back(pt.x);
        _spline.data.vdata.push_back(pt.y);
    }
    _spline.Upload();
    return true;
}


std::vector<sPoint> gui::cSpline::GenerateSegments()
{
   sPoint p1 = { _controlPoints[1].x, _controlPoints[1].y };
   sPoint p2 = { _controlPoints[2].x, _controlPoints[2].y };
   float dx = (p2.x > p1.x) ? (p2.x - p1.x)/20 : -(p1.x - p2.x)/20;
   float dy = (p2.y > p1.y) ? (p2.y - p1.y)/20 : -(p1.y - p2.y)/20;
   std::vector<sPoint> pts;       
   pts.push_back(_controlPoints[0]);
   pts.push_back(_controlPoints[1]);
   for (int i = 0; i < 20; i++)
   {
       p1.x += dx;
       p1.y += dy * (i * 0.1f);
       pts.push_back(p1);
   }
   for (int i = 20; i > 0; i--)
   {
       p1.x += dx;
       p1.y += dy * (i * 0.1f);
       pts.push_back(p1);
   }
   pts.push_back(_controlPoints[4]);
   return pts;
}

My cLine class has the every day VAO/VBO/vertex descriptor goodness.
The cSpline class just adds the control point concept and fills the in between.
p1 is the start position. p2 is a calculated mid point relative to the end position.
dx and dy is the linear step value between p1 and p2.
The curve comes from the vertical scaling during the two for loops in GenerateSegments.
Pretty simple and effective in all four directional quadrants.

1 likes 8 comments

Comments

NubDevice

The extra copy is for the copy/paste guys and gals. :P
Took that out this morning.

January 15, 2025 02:42 PM
Alberth
float dx = (p2.x > p1.x) ? (p2.x - p1.x)/20 : -(p1.x - p2.x)/20;

Both terms are the same: “-(a - b)” is the same as “b - a”

January 16, 2025 07:38 AM
NubDevice

Hot digity… it's true.
I was thinking , enforce "take from the larger”
Nice one.

January 16, 2025 01:32 PM
JoeJ

std::vector<sPoint> pts;

You already know how many points there will be, so you could reserve() to minimize allocations. (same in other places.)

My cLine class has the every day VAO/VBO/vertex descriptor goodness.

Meaning, each little spline gets its own unique buffers? Not sure how good that is. You could have just one buffer for lines, and upload / draw them in one batch. (I do this for all my debug visuals, having buffers for colored points, lines and triangles. But even uploading just that is slow because i do it every frame.)

Typically, what I saw was the sharp bent line.

I've read the best option is considered to be neither straight lines nor curves, but actually axis aligned line segments doing sharp 90 degrees turns, similar to a circuit board. The advantage is that crossings can be avoided / reduced this way, so it's easier for the user to follow the connections.

However, that's pretty complicated. Personally i would just give each curve a random color.

January 17, 2025 09:53 AM
NubDevice

ouch. KISS straight out the second story window. You bet, I'm game.
Single draw call per primitive type you say. yup that sounds good.
I suppose this is a great opportunity to finally experiment with glBufferSubData and friends.
I'd have to learn how to not draw a range when an end is being dragged and maybe have a separate 'selected/editing' buffer ping/pong thing. My color comes from uniform so there is something else to ponder to not loose onHover behavior.
Thinking about it, I guess glDrawArrays would do that just fine reducing all the draw calls to one or three draw calls when editing a node connector or two if you were at the first or last object in the buffer. I can understand the full buffer push every frame to avoid stalls. (it's ‘kinda a neat exercise to tackle) I’m a little bummed that I won't be able to use GL_LINE_STRIP anymore and my buffers get a little fatter.

I am liking the 90° line turn thing. (we're adding that style for certain)
Reserve sounds good as well, although probably should drop std::vector all together because sizes are fixed as you say.

This whole side project started because I couldn't figure out how to owner draw a title bar background and said F Windows, I'll do it myself.

January 17, 2025 11:53 AM
JoeJ

I suppose this is a great opportunity to …

Ugh, that all sounds like a lot of gfx API frustration, and i would feel guilty.
I would just add a comment about potential optimization but don't change anything. : )
It works and only is for the editor. Performance may never become a real problem.

Reserve sounds good as well, although probably should drop std::vector all together because sizes are fixed as you say.

Yeah, but std::vector is so convenient to use, and it also means less code to maintain.
And again it's about editor, so…
Personally i'm quite sloppy with optimization on the editor, and i use std::vector for everything.
But i try to make some things a habit: Preserving where possible, or avoididing redundant copies for example.
It's low hanging fruits which do not really cause extra work.

This whole side project started because I couldn't figure out how to owner draw a title bar background and said F Windows, I'll do it myself.

Tried ImGui? It's really a godsent. No more need to work on GUI myself.
Allthough - it can't do node graphs. But there would be some other libs to add this as well.

January 17, 2025 02:47 PM
NubDevice

Re: Dear ImGui - Currently using that in a Vulkan implementation that I keep under wraps.
I used to be a “glue everything(libs) together type of guy” but today I'm leaning more towards reinventing wheels. I think it partly has to do with the 2019 crowd here, they tended to scoff at it. Honestly, I ‘kinda don’t like the global namespace pollution, but that's about it. Immediate mode gui is not a bad concept. Same with freetype. Currently using it on this thing, but I'm thinking about moving back to bitmap font. I don't know. The ease of scaling might be hard to let go of and it really does look crisp. Oh, and assimp. Same. Just desimates the class viewer.

Thank you both for the interaction. Some of the things said here are still worth trying. If only for the exercise. Programming is not my profession so I'm free to dabble a little.

January 18, 2025 12:35 AM
NubDevice

Progress Update:

Using glBufferSubData, custom drag routines, onHover line coloring, One vertex buffer.
Thanks, this project is fun.

I don't know why I've avoided combined buffers for so long. Probably something to do with ‘got it working, move on to the next thing' mentality. When wearing all the hats, everything gets blurred together. Pretty straight forward in ogl. Track an offset/length and good to go. Draw range or dump the whole thing to the render.

So, yeah.. I'm set dragging out new links now, adding them to the list. Moving to the actual node frames. Tinkering with the appearance atm, thinking about how I want to handle the data to data / callback or whatever the needs may be.

January 26, 2025 08:22 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement
Advertisement