Drawing a repeated primitive along a path

Hi guys,

So I have a simple path roughly in the shape of a circle and I want to repeat a texture around the entire length of the path. I'm using primitives because ultimately the path I'm going to apply this to is massive and this is the most efficient way so far to achieve this.

Using a simple for loop I've managed to create a primitive made up of lots of alternating triangles but I need the repeating pattern to properly rotate and form around the the path. If you see the image below it will make a lot more sense.

The issue occurs at 3 o'clock and at 9 o'clock where the primitive sort of twists back on itself.

If I extend down vertically then the effect is even more dramatic and you basically just see a thin slither.


Below is the code I've come up with so far but I'm struggling to get the triangles to angle right. I'm sure length_dirx and y is along the right lines but so far I'm drawing a blank.

Code:
//Create event

even_check = -1;
triangle_size = 10;
xpos = 0;
ypos = 0;
Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use

texture_set_repeat(true);
var tex = sprite_get_texture(sprite3,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments

for (i= 0; i<= 1; i+= 0.001)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
 
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos_next = path_get_x(path1, i+0.001);
    ypos_next = path_get_y(path1, i+0.001);
 
    //Get the current angle

    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next);
 
    if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        xpos = path_get_x(path1, i) + triangle_size;
        ypos = path_get_y(path1, i) - triangle_size;
   
        xpos = xpos + lengthdir_x(triangle_size,current_angle);
        ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos, ypos,0,0);
   
        even_check = 0;
    }
    else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        xpos = path_get_x(path1, i) + triangle_size;
        ypos = path_get_y(path1, i) + triangle_size;
   
        xpos = xpos + lengthdir_x(triangle_size,current_angle);
        ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos, ypos,0,1);
   
        even_check = -1;
    }
 
}

draw_primitive_end();
 
Last edited:

jo-thijs

Member
Hi guys,

So I have a simple path roughly in the shape of a circle and I want to repeat a texture around the entire length of the path. I'm using primitives because ultimately the path I'm going to apply this to is massive and this is the most efficient way so far to achieve this.

Using a simple for loop I've managed to create a primitive made up of lots of alternating triangles but I need the repeating pattern to properly rotate and form around the the path. If you see the image below it will make a lot more sense.

The issue occurs at 3 o'clock and at 9 o'clock where the primitive sort of twists back on itself.

If I extend down vertically then the effect is even more dramatic and you basically just see a thin slither.


Below is the code I've come up with so far but I'm struggling to get the triangles to angle right. I'm sure length_dirx and y is along the right lines but so far I'm drawing a blank.

Code:
//Create event

even_check = -1;
triangle_size = 10;
xpos = 0;
ypos = 0;
Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use

texture_set_repeat(true);
var tex = sprite_get_texture(sprite3,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments

for (i= 0; i<= 1; i+= 0.001)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
 
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos_next = path_get_x(path1, i+0.001);
    ypos_next = path_get_y(path1, i+0.001);
 
    //Get the current angle

    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next);
 
    if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        xpos = path_get_x(path1, i) + triangle_size;
        ypos = path_get_y(path1, i) - triangle_size;
  
        xpos = xpos + lengthdir_x(triangle_size,current_angle);
        ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos, ypos,0,0);
  
        even_check = 0;
    }
    else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        xpos = path_get_x(path1, i) + triangle_size;
        ypos = path_get_y(path1, i) + triangle_size;
  
        xpos = xpos + lengthdir_x(triangle_size,current_angle);
        ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos, ypos,0,1);
  
        even_check = -1;
    }
 
}

draw_primitive_end();
Close. Here's a working version:
Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use

//texture_set_repeat(true);
var tex = sprite_get_texture(sprite0,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments

di = 0.005;
path = path0;

xpos_next = path_get_x(path, 0) * 2 - path_get_x(path, di);
ypos_next = path_get_y(path, 0) * 2 - path_get_y(path, di);

for (i = 0; i <= 1; i += di)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
 
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos = xpos_next;
    ypos = ypos_next;

    xpos_next = path_get_x(path, i);
    ypos_next = path_get_y(path, i);
 
    //Get the current angle

    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next) + 90;

    dx = lengthdir_x(triangle_size, current_angle);
    dy = lengthdir_y(triangle_size, current_angle);
 
    //if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        //xpos = path_get_x(path0, i) + triangle_size;
        //ypos = path_get_y(path0, i) - triangle_size;
  
        //xpos = xpos + lengthdir_x(triangle_size,current_angle);
        //ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos + dx, ypos + dy, 0, 0);
  
        //even_check = 0;
    }
    //else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        //xpos = path_get_x(path0, i) + triangle_size;
        //ypos = path_get_y(path0, i) + triangle_size;
  
        //xpos = xpos + lengthdir_x(triangle_size,current_angle);
        //ypos = ypos + lengthdir_y(triangle_size,current_angle);

        draw_vertex_texture(xpos - dx, ypos - dy, 0, 1);
  
        //even_check = -1;
    }
    
    if (i == 0)
    {
        x1 = xpos + dx;
        y1 = ypos + dy;
        x2 = xpos - dx;
        y2 = ypos - dy;
    }
}

if path_get_closed(path)
{
    draw_vertex_texture(x1, y1, 0, 0);
    draw_vertex_texture(x2, y2, 0, 1);
}

draw_primitive_end();
Your approach requires a lot of verttices though. It's really pushing to the limit that my computer can handle.
Increasing the detail or the path length further makes a part of the path not being drawn.
 
Hi Jo

In your response a lot of the even_check if statement is commented out. I've tried uncommenting all of your lines but just get a seemingly random assortment of black zig-zags.

Also, I appreciate that a lot of vertices are being drawn, but I'm only drawing for the entire path in this demonstration. In my game, I'll only actually ever draw a little bit of the path at a time.
 
I'm also confused as to why your initial declaration of xpos_next and ypos_next is outside of the for loop? Am I missing something?
Thanks.
 
Never mind. I've fixed it, it was because your example had changed the texture sprite, path and had commented out the texture repeat line. I just want to wrap my head around your changes now. Thanks ever so much.
 
OK. So I've added an object that follows the path and modified the for loop to draw just in front of and just behind this object. The problem now is this happens:



Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use
texture_set_repeat(true);
var tex = sprite_get_texture(sprite3,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments
di = 0.005;
path = path1;

xpos_next = path_get_x(path, 0) * 2 - path_get_x(path, di);
ypos_next = path_get_y(path, 0) * 2 - path_get_y(path, di);

player_previous = object0.current_position_along_path - 0.1;
player_next = object0.current_position_along_path + 0.1;

for (i = player_previous; i <= player_next; i += di)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos = xpos_next;
    ypos = ypos_next;
    xpos_next = path_get_x(path, i);
    ypos_next = path_get_y(path, i);

    //Get the current angle
    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next) + 90;
    dx = lengthdir_x(triangle_size, current_angle);
    dy = lengthdir_y(triangle_size, current_angle);

    if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        draw_vertex_texture(xpos + dx, ypos + dy, 0, 0);
 
        even_check = 0;
    }
    else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        draw_vertex_texture(xpos - dx, ypos - dy, 0, 1);
 
        even_check = -1;
    }
   
}

draw_primitive_end();
The problem I was having above was that for some reason when you pasted in your modified code, for some reason, random lines were commented out.

Couple of questions, though:

Why do you initially declare xpos_next and ypos_next outside of the path loop?
Why do you declare xpos and ypos as the next versions inside the loop?
I take it that in your original code the if i==0 loop was to close the little gap that was created at the start of my original demo?
 

jo-thijs

Member
Never mind. I've fixed it, it was because your example had changed the texture sprite, path and had commented out the texture repeat line. I just want to wrap my head around your changes now. Thanks ever so much.
Oh oops, forgot to change that back.

Why do you initially declare xpos_next and ypos_next outside of the path loop?
I declare them that way because in the for loop I assign values to xpos and ypos in the following lines only:
Code:
    xpos = xpos_next;
    ypos = ypos_next;
These require xpos_next and ypos_next to have values in advance.
I can't just assign them values in the create event though, as their values are used to determin where the first two vertices are drawn.
I made a mistake though, xpos_next and ypos_next should have been initialized to path_get_x(path, 0) and path_get_y(path, 0)
and the for loop should have started at i = di.
I guess I was thinking xpos_next and ypos_next were used instead of xpos and ypos as center of the first two vertices when I wrote those two lines.

Why do you declare xpos and ypos as the next versions inside the loop?
Because that's what they are, no?
This way you save recalculating path positions (which is faster of course).
It also got rid of some code duplication (for example, the multiple occurrences of the magic number 0.001, which I replaced by di anyway).

I take it that in your original code the if i==0 loop was to close the little gap that was created at the start of my original demo?
Correct.

OK. So I've added an object that follows the path and modified the for loop to draw just in front of and just behind this object. The problem now is this happens:



Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use
texture_set_repeat(true);
var tex = sprite_get_texture(sprite3,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments
di = 0.005;
path = path1;

xpos_next = path_get_x(path, 0) * 2 - path_get_x(path, di);
ypos_next = path_get_y(path, 0) * 2 - path_get_y(path, di);

player_previous = object0.current_position_along_path - 0.1;
player_next = object0.current_position_along_path + 0.1;

for (i = player_previous; i <= player_next; i += di)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos = xpos_next;
    ypos = ypos_next;
    xpos_next = path_get_x(path, i);
    ypos_next = path_get_y(path, i);

    //Get the current angle
    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next) + 90;
    dx = lengthdir_x(triangle_size, current_angle);
    dy = lengthdir_y(triangle_size, current_angle);

    if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        draw_vertex_texture(xpos + dx, ypos + dy, 0, 0);
 
        even_check = 0;
    }
    else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        draw_vertex_texture(xpos - dx, ypos - dy, 0, 1);
 
        even_check = -1;
    }
  
}

draw_primitive_end();
The problem I was having above was that for some reason when you pasted in your modified code, for some reason, random lines were commented out.
The issue is with the initialization of xpos_next and ypos_next.
Try this:
Code:
//Draw event

//Set up our primitive, enable texture repeat and define the texture we will use
texture_set_repeat(true);
var tex = sprite_get_texture(sprite3,0);
draw_primitive_begin_texture(pr_trianglestrip,tex);

//Loop through from 0 to 1 (1-100% of the path) in small increments
di = 0.005;
path = path1;

player_previous = object0.current_position_along_path - 0.1;
player_next = object0.current_position_along_path + 0.1;

xpos_next = path_get_x(path, player_previous);
ypos_next = path_get_y(path, player_previous);

for (i = player_previous + di; i <= player_next + di; i += di)
{
    //We alternate even_check between -1 and 0 to swap the orientation of the
    //triangles we are drawing each iteration
    //
    //   /\/\/\/\/\/\/\/\/
    //
    //We get the xpos and ypos from just slightly in front of our current x and y pos
    //so we can determine a close-enough approximation of the current angle

    xpos = xpos_next;
    ypos = ypos_next;
    xpos_next = path_get_x(path, i);
    ypos_next = path_get_y(path, i);

    //Get the current angle
    current_angle = point_direction(xpos, ypos, xpos_next, ypos_next) + 90;
    dx = lengthdir_x(triangle_size, current_angle);
    dy = lengthdir_y(triangle_size, current_angle);

    if (even_check == -1)
    {
        //To draw an upward pointing triangle we increment xpos by triangle size
        //And decrement ypos by the triangle size

        draw_vertex_texture(xpos + dx, ypos + dy, 0, 0);
 
        even_check = 0;
    }
    else
    {
        //To draw a downward pointing triangle we increment xpos and ypos by triangle size

        draw_vertex_texture(xpos - dx, ypos - dy, 0, 1);
 
        even_check = -1;
    }
  
}

draw_primitive_end();
 
Thanks ever so much for your help on this jo-thijs - I will try your update when I get home from work tonight.

As an aside, I very much appreciate your point about hindering performance. The whole reason I've ended up exploring vertices was because performance was far worse with every other solution I've tried so far.

Originally I tried instancing lots of objects along my path - this was slow.
Then I tried spawning lots of sprites along my path - this was better but still slow.
Then I tried surfaces, but because my room size is 50,000 x 50,000 this causes hard crashes. Surfaces don't like to be much beyond 10,000 pixels I believe.

Do you have another suggestion as to how I might achieve this effect or do you think an optimised version of this is probably my best solution?
 

jo-thijs

Member
Then I tried surfaces, but because my room size is 50,000 x 50,000 this causes hard crashes. Surfaces don't like to be much beyond 10,000 pixels I believe.
I believe it depends on the client hardware.
50000 x 50000 is huge for pretty much any commercial hardware though.
We may assume every pixel takes up 4 bytes.
This surface would then take up 10 000 000 000 bytes, which is 10 gigabytes.
My pc has a vram of about 8 gigabytes, so it wouldn't be able to load that.

Do you have another suggestion as to how I might achieve this effect or do you think an optimised version of this is probably my best solution?
The approach you're currently taking should already be pretty performant.
Try to get as much as possible drawn in a single draw call (by using primitives for example) to optimally use the GPU.
Furthermore, try to minimize the amount of vertices you use (by for example not drawing vertices out of view).

A way you might be able to reach higher performance, is by using vertex buffers.
If you never change the path, you can calculate all of its vertices once, store it into a vertex buffer and freeze the vertex buffer to make it as efficiently as possible for the GPU to use.
To avoid having to draw the entire path every time again, you could try split up the path in a grid of say 512 x 512 pixels cells.
If your path is 5000 x 5000, this would result in a 10 x 10 grid of vertex buffers.
You then only draw the buffers that are in a cell that overlaps with the view.

Another way you might save some performance is by understanding how GameMaker paths work.
I've studied them a while back and came to the conclusion that they consist of line segments only.
The amount of line segments depend on the amount of path points, whether the path is closed and/or smooth and the path precision.
In general, the end points of the line segments are calculated by using spline curves (of second degree if smooth and first degree otherwise).
There were some awkward edge cases however where GameMaker suddenly used something different (the odds are you won't encounter those edge cases however).
GameMaker uses more detail for parts of the path determined by close consecutive points, then for parts determined by consecutive points that are far away.
In these parts, you are using many vertices to draw a single line, which is somewhat stupid, as you only need 4 vertices for a single line (with a width).
By figuring out which line segments the path consists of, you only have to use 2 vertices per line segment, which could save you a lot of vertices.
For example, in the example I tested, I would have used 136 vertices, rather than 404.
This approach only works well though if your texture is a single vertical line (as it is in the images you posted).
 
That's all very insightful and I appreciate you taking the time to provide such a comprehensive answer.

The sprite I'm using in my example isn't actually a single vertical line - I haven't yet got as far as working out how to tile it properly. The ytex and xtex uv points aren't explained particularly well in the documentation I've read so far.

If you skip to about 1 minute 30 in this video you will see what I'm trying to achieve.


My plan is to only draw enough of the path to cover the whole of the screen. I wondered about just drawing the whole entire path to a single surface and navigating along that, but that's when I ran into the sizing issues.
 

jo-thijs

Member
That's all very insightful and I appreciate you taking the time to provide such a comprehensive answer.

The sprite I'm using in my example isn't actually a single vertical line - I haven't yet got as far as working out how to tile it properly. The ytex and xtex uv points aren't explained particularly well in the documentation I've read so far.

If you skip to about 1 minute 30 in this video you will see what I'm trying to achieve.


My plan is to only draw enough of the path to cover the whole of the screen. I wondered about just drawing the whole entire path to a single surface and navigating along that, but that's when I ran into the sizing issues.
My advice is especially applicable to what's shown in the video.
They only use a singe line texture too (if they even use a texture at all).

1) Go for primitives.
2) Only draw segments that are in view.
Store the path positions of the start and end of every piece in an array in chronological / increasing order.
Keep track of the array index of the piece the player is currently in.
Only draw the pieces starting from that index up to the first piece where the path position exceeds the player's path position + some fixed distance.
3) Split the path up in the line segments it consists of and only use 2 triangles per line segment.
In the video, most parts of the path were almost entirely straight, so only a few vertices need to be used.
 
Excellent, thanks again. You've been a god-send. I really appreciate it. I've been banging my head against this problem for about 9 months on and off.

I've already got the notes, timing and audio synching sorted, it's just taken me forever to get to primitives as a solution for the path lines, but now I'm starting to understand it a bit more, hopefully I'll be able to knock it into shape and have the performance smooth and consistent. :)
 
Top