Most efficient way to draw lots of line

Hi! I want to make a Zatacka clone.

I am now trying to draw the line laid by the players, but the framerate drop waayyyyy too quickly (hard on the gpu).
Every tick I am saving the position of the player (reference for collision) and every 0.01sec, a task draw a line between the most rescent dot and the last drawn dot.

What would be an efficient way to do this? I know mine doesn’t work :stuck_out_tongue:

def draw_line(self, task):
    for entity_id, entity in configs.ENTITIES.items():
        if entity['CATEGORY'] == 'player':
            last_pos_x = entity['LAST_DRAWN_LINE'][0]
            last_pos_y = entity['LAST_DRAWN_LINE'][1]
            new_pos_x = entity['DOTS'][-1][0]
            new_pos_y = entity['DOTS'][-1][1]
            ls = LineSegs("lines")
            ls.setColor(1,1,1,1)
            ls.drawTo(last_pos_x, 0, last_pos_y)
            ls.drawTo(new_pos_x, 0, new_pos_y)
            ls.setThickness(2)
            line = ls.create(False)
            nodePath = NodePath("line")
            nodePath.attachNewNode(line)
            nodePath.reparentTo(aspect2d)
            entity['LAST_DRAWN_LINE'] = (new_pos_x,new_pos_y)
    
    task = taskMgr.doMethodLater(0.1, draw_line, 'draw_line', extraArgs=[self], appendTask=True)
    return task.done

Thank you!

Don’t reschedule the task function at the end of the task. Schedule it once in your init or some ‘start’ method.

(somewhere in __init__)
    self.line_task = taskMgr.doMethodLater(0.1, self.draw_line, 'draw_line')

You’ll notice I referenced it as a bound method rather than as a function. That way you can do away with the extra args.

(at the end of draw_line)
    return task.again

Return task.again at the end of the task function, and the task manager will automatically reschedule it with the same delay.

I don’t know that this resolves your issue, but it could be that you were accidentally scheduling the same task many times over and not noticing because it was redrawing lines on top of existing lines.

Okay, upon further review I know what’s wrong. You are supposed to describe all of the line segments and then call LineSegs.create for the whole thing, not for each individual segment. So create the LineSegs object once in your init or elsewhere. It will remember the previous point internally so you can probably remove that bookkeeping from your code. I also create the node once and use replaceNode to update it. I hope that works (completely untested)! So, combined with my previous advice…

(somewhere in __init__)
    self.ls = LineSegs("lines")
    self.ls.setColor(1,1,1,1)
    self.ls.setThickness(2)
    self.lines_nodepath = aspect2d.attachNewNode("lines")
    self.line_task = taskMgr.doMethodLater(0.1, self.draw_line, 'draw_line')

def draw_line(self, task):
    for entity_id, entity in configs.ENTITIES.items():
        if entity['CATEGORY'] == 'player':
            new_pos_x = entity['DOTS'][-1][0]
            new_pos_y = entity['DOTS'][-1][1]
            self.ls.drawTo(new_pos_x, 0, new_pos_y)
            lines = self.ls.create(False)
            lines.replaceNode(self.lines_nodepath.node())
            entity['LAST_DRAWN_LINE'] = (new_pos_x,new_pos_y)
    return task.again

EDIT:
The code I wrote above only works for 1 player. You’ll want to either store a different LineSegs instance for each player or reinsert the lines remembering previous points and use moveTo instead of drawTo when switching to the next player.

If you do that, only the last line segment is drawn. There is however an alternative LineSegs.create() method that accepts an existing GeomNode to append the new segments to.
I made a code sample that draws a line as it follows the mouse cursor (not precisely, as I did not take the aspect ratio into account, but it’s just an example):

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
loadPrcFileData("", """
                    sync-video #f

                    """
               )


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.set_frame_rate_meter(True)
        self.last_pos = Point2()
        self.line_segs = ls = LineSegs("path")
        ls.set_color(1., 1., 1.)
        ls.set_thickness(2)
        ls.move_to(0., 0., 0.)
        self.path_geom = GeomNode("path")
        self.path = self.aspect2d.attach_new_node(self.path_geom)

        self.taskMgr.doMethodLater(0.1, self.__draw_path, "draw_path")
        
    def __draw_path(self, task):

        if not self.mouseWatcherNode.has_mouse():
            return task.again

        pos = self.mouseWatcherNode.get_mouse()
        
        if pos != self.last_pos:
            x, y = self.last_pos
            self.line_segs.move_to(x, 0., y)
            x, y = self.last_pos = Point2(pos)
            self.line_segs.draw_to(x, 0., y)
            self.line_segs.create(self.path_geom)
        
        return task.again
        

app = MyApp()
app.run()

It works, but it slows down fairly quickly as more line segments are drawn.

A much more efficient way I’ve found is to use the low-level GeomLines primitive directly. Even when the screen is filled with line segments, the frame rate hardly drops. It’s just a single GeomPrimitive, whereas using a LineSegs object creates a new Geom for every new segment.
Here is the code:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
loadPrcFileData("", """
                    sync-video #f

                    """
               )


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.set_frame_rate_meter(True)
        self.last_pos = Point2()
        self.new_pos = Point2()
        self.path_prim = GeomLines(Geom.UH_static)
        vertex_format = GeomVertexFormat.get_v3()
        self.path_vertex_data = GeomVertexData("path_data", vertex_format, Geom.UH_static)
        pos_writer = GeomVertexWriter(self.path_vertex_data, "vertex")
        pos_writer.add_data3f(0., 0., 0.)
        geom = Geom(self.path_vertex_data)
        geom.add_primitive(self.path_prim)
        geom_node = GeomNode("path")
        geom_node.add_geom(geom)
        self.path = path = self.aspect2d.attach_new_node(geom_node)
        path.set_color(1., 1., 1.)
        path.set_render_mode_thickness(2)

        self.taskMgr.doMethodLater(0.1, self.__draw_path, "draw_path")
        
    def __add_path_segment(self, point):

        pos_writer = GeomVertexWriter(self.path_vertex_data, "vertex")
        rows = self.path_vertex_data.get_num_rows()
        pos_writer.set_row(rows)
        pos_writer.add_data3f(point)
        prim = self.path_prim
        prim.add_vertices(rows - 1, rows)
        
    def __draw_path(self, task):

        if not self.mouseWatcherNode.has_mouse():
            return task.again

        pos = self.mouseWatcherNode.get_mouse()
        
        if pos != self.last_pos:
            x, y = self.last_pos = Point2(pos)
            point = Point3(x, 0., y)
            self.__add_path_segment(point)
        
        return task.again
        

app = MyApp()
app.run()

Hope this helps.

Thanks! Exactly what I was looking for! I tought that LineSegs was a quick GeomLine tool. Apparently, they are not the same!

The task.again call is a nice thing to know. It make more sense that what I was doing…

Thanks again both of you, that was really helpfull!