GeomLinestrips stops rendering if any other node is rendered off camera

Hi, very new to Panda3d (using 1.10.3) and running into a confusing issue. I am trying to add motion trails to objects in my world, and running into a problem where any object that is rendered outside of the camera view prevents any motion trails inside the camera view from rendering, regardless of whether the object outside the camera view had a motion trail attached to it.

I was unable to get the direct.motiontrail.MotionTrail class to work (anyone know what vertex fuction/context is, and how to use them to add vertices?), and so I have written my own. It just uses a task to poll the global coordinates of the attached object at a specified interval, and generates a GeomLinestrips line connecting all the points. When the maximum number of coordinates is reached, it discards the oldest one. Implementation is here:

from panda3d.core import *
from direct.task import Task
from direct.showbase.DirectObject import DirectObject
from collections import deque

class MotionTrail(DirectObject):

    task_added = False
    motion_trail_list = [ ]
    motion_trail_task_name = "motion_trail_task"

    def __init__(self, leading_nodepath):
        self.vertex_max = 50
        self.leading_nodepath = leading_nodepath
        self.center = NodePath("center")
        self.center.setPos(0,0,0)
        self.verticies = deque([])
        self.geom_vertex_format = GeomVertexFormat.getV3c4()
        self.renderThickness = 1
        self.update_interval_s = 0.1
        self.last_update_s = 0

        self.prim = GeomLinestrips(Geom.UHStatic)
        self.geom_vertex_data = GeomVertexData("Motion Trail", self.geom_vertex_format, Geom.UHStatic)
        self.geom_vertex_data.setNumRows(self.vertex_max)
        geom = Geom(self.geom_vertex_data)
        geom.addPrimitive(self.prim)

        geomNode = GeomNode("Motion Trail Geom Node")
        geomNode.addGeom(geom)

        self.nodePath = render.attachNewNode(geomNode)
        self.nodePath.setRenderModeThickness(self.renderThickness)
        
        MotionTrail.motion_trail_list.append(self)

        if MotionTrail.task_added == False:
#            taskMgr.add (self.motion_trail_task, "motion_trail_task", priority = 50)
            taskMgr.add(self.motion_trail_task, MotionTrail.motion_trail_task_name)

            self.acceptOnce("clientLogout", remove_task)

            MotionTrail.task_added = True

    
    def renderTrail(self):
        if len(self.verticies) < 2:
            return

        self.prim.clearVertices()
        vertex = GeomVertexWriter(self.geom_vertex_data, "vertex")
        color = GeomVertexWriter(self.geom_vertex_data, "color")
        print("########################################")
        for index, vertice in enumerate(self.verticies):
            print("x: " + str(vertice.x) + " y: " + str(vertice.y) + " z: " + str(vertice.z))
            vertex.addData3f(vertice.x, vertice.y, vertice.z)
            color.addData4f(1,1,1,1) #max(float(self.vertex_max - index), 1)/self.vertex_max
        print("########################################")
        
        self.prim.addConsecutiveVertices(0, len(self.verticies))
        self.prim.closePrimitive()

    def motion_trail_task(self, task):

        if task.time - self.last_update_s > self.update_interval_s:
            self.last_update_s = task.time

            for motion_trail in MotionTrail.motion_trail_list:
                motion_trail.verticies.append(motion_trail.leading_nodepath.getPos(motion_trail.center))
                if len(motion_trail.verticies) > motion_trail.vertex_max:
                    motion_trail.verticies.popleft()

                motion_trail.renderTrail()

        return Task.cont

    def destroy(self):
        MotionTrail.motion_trail_list.remove(self)

        if len(MotionTrail.motion_trail_list) == 0 and MotionTrail.task_added:
            remove_task()

    def IsInView(self, other_node):
        lensBounds = base.cam.node().getLens().makeBounds()
        bounds = other_node.getBounds()
        bounds.xform(other_node.getParent().getMat(base.cam))
        return lensBounds.contains(bounds)

##########################END MOTION TRAIL CLASS##########################
def remove_task():
    if MotionTrail.task_added:
        total_motion_trails = len(MotionTrail.motion_trail_list)

        if total_motion_trails > 0:
            print("warning: %d motion trails still exist when motion trail task is removed" % (total_motion_trails))

        MotionTrail.motion_trail_list = [ ]

        taskMgr.remove(MotionTrail.motion_trail_task_name)

        print("MotionTrail task removed")

        MotionTrail.task_added = False
    return

This works fine as long as all objects attached to ‘render’ are well within the view, but starts to falter when they are close to the edge and stops rendering outright when they are no longer visible. Here is a sample program that illustrates the issue:

from direct.showbase.ShowBase import ShowBase
base = ShowBase()

from motion_trail import MotionTrail

class World():
    def __init__(self):
        base.setBackgroundColor(0, 0, 0)
        base.disableMouse()
        camera.setPos(0, 0, 90)
        camera.setHpr(0, -90, 0)

        self.visible_model = loader.loadModel("models/sphere")
        self.visible_motion_trail = MotionTrail(self.visible_model)
        self.visible_model.reparentTo(render)
        self.visible_model.setPos(10, 0, 0)
        self.visible_model.posInterval(5, (-10, 0, 0)).start()

        self.invisible_model = loader.loadModel("models/sphere")
        self.invisible_model.reparentTo(render)
        self.invisible_model.setPos(40, 0, 0)

world = World()
base.run()

Setting the “invisible model” position to 40 will have the visible model traverse the screen without a trail. Setting the “invisible model” position to 30 will have it right at the edge of the screen, and the visible model will have a render trail that eventually cuts out. Setting the “invisible model” position to 20 will have the render trail stay with the visible model for its entire traversal, working as intended. Setting the “invisible model” position at 40 but no parenting it to ‘render’ will have the motion trail work as intended.

Any help or advice on how to solve this issue? Thanks.

Hi, welcome to the forums!

I realise this behaviour may seem a little unintuitive, but you have to re-acquire access to the GeomPrimitive and GeomVertexData when you modify it in the next frames, otherwise the GeomNode won’t know that it has been changed and therefore won’t recalculate the bounding volume.

Try this in renderTrail:

        geom = self.nodePath.node().modify_geom(0)
        geom_vertex_data = geom.modify_vertex_data()
        prim = geom.modify_primitive(0)

On a sidenote, I do recommend that you update to 1.10.4.1 since it has a number of bug fixes over 1.10.3.

Worked like a charm, thanks for the fast response. Looks like my premature optimization for reusing the primitives has come back to haunt me, root of all evil indeed.

That said, I am still a bit confused as to why the issue manifested itself this way. If I was not re-acquiring a particular GeomPrimitive on every frame, why was its shape still changing until it went off screen instead of just not working at all, and why did one GeomPrimitive going off screen affect all of the other ones? Since I declared them as instance variables, I assume that they should operate independent of each other.

I will also get on that later version, thanks for the heads up about it.