Particle effects and Collision Solids

Hi to all,

I was wondering if anyone has any experience with using the existing particle system along with collision solids [as compared to creating their own particle system]. What I was thinking was to encapsulate an entire particle effect within a collision solid such as a cube or a sphere. For instance, if the particle effect is that of fire, and I want an NPC or any other object to get hurt each time they pass within it, then obviously a collision solid would be very useful; if the NPC/other object is colliding with, or is “inside” the collision solid, then it’d take damage. Trying to however get the bounds of a particle system [getBounds()/getTightBounds()] so that I can use them to create either a collision-sphere or a collision-cube has proved futile, as both methods return “None”. Why can’t a particle effect have bounds? Such a thing would, from a practical standpoint, prove to be highly useful. And since one can’t use that technique to encapsulate a particle effect, is there any other way one can properly use a particle effect with collision solids in order to fit it into the game’s logic?

Thanks.

EDIT:

It just occurred to me that one could use a cube/sphere model, create a collision solid from it, parent it to the particle effect in question and then manually manipulate it’s size so that it encapsulates the entire particle effect. So any other transform changes performed on the particle effect would reflect in the collision solid. That would work, but it’d still be nice if particle effects had bounds.

With some math on particle velocities, forces, and lifetimes, you should (in most cases trivially) be able to determine the radius (or bounding box) of a particle effect.

Has nothing changed in this case? I found another thread dealing with a similar problem and even asked a question there, but no answer.

Hey, the edit to my question proved to be a useful workaround at the time, I hope it aids you in whatever it is you’re hoping to achieve.You’d basically encapsulate your entire particle effect inside a collisionBox or collisionSphere for example initially. Then, you’d parent the collisionNodePath associated with the collisionBox/collisionSphere to the particle effect. That way, any transform changes that would happen to the particle effect would also happen to the collisionNodePath. Of course you’d have to associate your collisionNodePath with a collisionHandler, such as a queue and query the results from it as necessary. That’s what I did as a workaround, I don’t know if it helps.

@Game_Starter Thank you for your answer. I do not know a bit about the Collisions system in Panda3D yet, but by what you wrote, do you mean that Particles in Panda3D DO HAVE or they DO NOT HAVE interactions (collisions) with other objects? Maybe I will explain it on an example that I saw in some demonstrations of (probably) Unreal Engine. We have objects (trees) on which some precipitation falls:

Snow? Snow in the summer? It makes no sense, but never mind. :slight_smile:
As you can see, the snow, flying from the sky, settles on what meets the first on its way (trees, grass). There is no snow under the trees (the leaves stopped it).
Meanwhile, normally in Panda3D, when I make snow from Particles, it will fly through the tree and even through the grass and penetrate the ground.
Will your way with CollisionSphere or CollisionBox cause my snow to land on these invisible Collision Solids?
Or maybe you have an example (screenshot) of how it works for you?
Sorry again if I ask the obvious things, but I’m still learning it.

To the best of my knowledge, the particles of Panda3D’s built-in particle system do not have collision capabilities.

I think that what Game_Starter is suggesting is that one could attach a collider to a particle effect such that any changes made to the effect–such as placing it in a new location–affect the collider.

I think that this would call for a custom system of some sort in Panda; I don’t think that we have anything built-in that would do quite that–at least with usable performance.

(Maybe a custom particle-module, which fires a ray whenever a particle is generated, and then stops each particle when it arrives at the location of that ray’s first hit. This would, of course, only work with particles that moved in a straight line.

More advanced systems might be possible, perhaps involving GPU-handling, or trickery with the colliders involved. However, I don’t currently know how those are done.)

No problem. First, you can read more about Panda3D’s collision system here, it’s very intuitive and easy to get into: Collision Detection — Panda3D Manual

Secondly, what @Thaumaturge said is correct, Panda3D’s particle effects don’t have any inbuilt collision capabilities, you would have to create something custom made for that. What I used at the time probably won’t work if you’re talking about individual pieces of snow. However, I happen to have had some free time this afternoon and so I implemented a rudimentary system that should do what you want to achieve. To have collision detection and a sort of particle effect, you’d have to abandon Panda3D’s particle system as it is currently and implement something custom made. To start with, here is a gif of it in action:

Animation_oo
[The artefacts and dark streaks in the gif are due to the screen-recording software I was using and have nothing to do with the sample itself.]

And here is the code:

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from direct.interval.IntervalGlobal import *
from panda3d.core import *
from direct.gui.DirectGui import *
import time
import math
import random,os,sys

class run_me(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        base.setFrameRateMeter(True)
        #A set of cubes can act as the "ground", define the dimension:
        #Draw them as the ground:
        self.cubeDimensions=Point3(240,240,8)
        colourA=LPoint4f(0,1,0,1)
        positionA=Point3(-120,-120,-20)
        self.spawnAGenericCube(colourA,positionA)
        #Draw a few other blocks to show that the particles do notice other objects:
        
        self.cubeDimensions=Point3(10,10,8)
        colourA=LPoint4f(1,0,0,1)
        positionA=Point3(-5,-15,-12)
        self.spawnAGenericCube(colourA,positionA)
        
        self.cubeDimensions=Point3(10,10,8)
        colourA=LPoint4f(1,0,0,1)
        positionA=Point3(5,15,-12)
        self.spawnAGenericCube(colourA,positionA)
        
        #Define a dimension for each particle:
        self.snowParticleDimension=1.5
        #Parameters to periodically spawn new snow particles:
        self.lastSpawnTime=0
        self.maxSnowParticleCapacity=200
        self.numSpawnedParticles=0
        self.spawnRangePointA=Point3(-50,-40,70)
        self.spawnRangePointB=Point3(50,40,90)
        #Parameters to drift each snow particle:
        self.driftForceRangeXYA=Point2(-2,-4)
        self.driftForceRangeXYB=Point2(8,10)
        self.driftForceRateChangeRangeXYA=Point2(-0.06,-0.05)
        self.driftForceRateChangeRangeXYB=Point2(0.08,0.06)
        self.minSnowPosition=Point2(-80,-80)
        self.maxSnowPosition=Point2(80,80)
        #Parameters to periodically kill the particles that have: landed and have been on the ground past a certain amount of time:
        self.snowParticleRestTime=4
        #For the collisions, traverser:
        self.traverser = CollisionTraverser('traverser')
        base.cTrav = self.traverser
        #A collisionHandlerQueue, to deal with modifying the z position of each snow particle:
        self.generalHandlerQueue=CollisionHandlerQueue()
        #The tasks:
        taskMgr.doMethodLater(0.15,self.taskToSpawnSnowParticle,"spawnSnowTask")
        taskMgr.add(self.taskToDriftParticles, 'driftSnowTask')
        taskMgr.add(self.taskToDropAndKillSnow, 'dropKillSnowTask')
        
    #--These tasks deal with spawning, drifting and killing snow particles:
    #1.Spawning particles periodically:
    def taskToSpawnSnowParticle(self,task):
        if(self.numSpawnedParticles<self.maxSnowParticleCapacity):
            #modify the dimension a bit:
            self.snowParticleDimension=random.uniform(0.5,1.5)
            newParticle=self.spawnASnowParticle()
            #set a position for it:
            randX=random.uniform(self.spawnRangePointA.x,self.spawnRangePointB.x)
            randY=random.uniform(self.spawnRangePointA.y,self.spawnRangePointB.y)
            randZ=random.uniform(self.spawnRangePointA.z,self.spawnRangePointB.z)
            newParticle.setPos(randX,randY,randZ)
            self.numSpawnedParticles+=1
        return task.again
    
    #2. Applying some simple drift-force in this case:
    def taskToDriftParticles(self,task):
        #First, find the snow particles:
        npCollection=render.findAllMatches("*snowParticleNumber*")
        listNps=npCollection.getPaths()
        for snowParticle in listNps:
            if(not snowParticle.hasPythonTag("hitGround")):
                currentDriftForce=snowParticle.getPythonTag("currentXYDriftForce")
                currentDriftRateOfChange=snowParticle.getPythonTag("currentXYDriftForceRateChange")
                snowParticle.setX(snowParticle,currentDriftForce.x*globalClock.getDt())
                snowParticle.setY(snowParticle,currentDriftForce.y*globalClock.getDt())
                #apply the change if possible:
                useXVector=currentDriftForce.x+currentDriftRateOfChange.x
                useYVector=currentDriftForce.y+currentDriftRateOfChange.y
                #print("X-DRIFT-D: ",useXVector,currentDriftForce.x,self.driftForceRangeXYA.x,self.driftForceRangeXYB.x)
                
                if(useXVector<self.driftForceRangeXYA.x or useXVector>self.driftForceRangeXYB.x):
                    #change the rate of change for x:
                    startDriftXRateChange=random.uniform(self.driftForceRateChangeRangeXYA.x,self.driftForceRateChangeRangeXYB.x)
                    driftXForce=random.uniform(self.driftForceRangeXYA.x,self.driftForceRangeXYB.x)
                    useXVector=driftXForce
                    snowParticle.setPythonTag("currentXYDriftForce",Point2(driftXForce,currentDriftForce.y))
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",Point2(startDriftXRateChange,currentDriftRateOfChange.y))
                if(useYVector<self.driftForceRangeXYA.y or useYVector>self.driftForceRangeXYB.y):
                    #change the rate of change for y:
                    startDriftYRateChange=random.uniform(self.driftForceRateChangeRangeXYA.y,self.driftForceRateChangeRangeXYB.y)
                    driftYForce=random.uniform(self.driftForceRangeXYA.y,self.driftForceRangeXYB.y)
                    useYVector=driftYForce
                    snowParticle.setPythonTag("currentXYDriftForce",Point2(currentDriftForce.x,driftYForce))
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",Point2(currentDriftRateOfChange.x,startDriftYRateChange))
                
                snowParticle.setPythonTag("currentXYDriftForce",Point2(useXVector,useYVector))
                if(snowParticle.getX()<self.minSnowPosition.x):
                    snowParticle.setX(self.minSnowPosition.x)
                    #use a positive vector:
                    currentDriftForce.x=self.driftForceRangeXYB.x
                    currentDriftRateOfChange.x=self.driftForceRateChangeRangeXYB.x
                    useXVector=currentDriftForce.x+currentDriftRateOfChange.x
                    snowParticle.setX(snowParticle,useXVector*globalClock.getDt())
                    snowParticle.setPythonTag("currentXYDriftForce",currentDriftForce)
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",currentDriftRateOfChange)
                if(snowParticle.getX()>self.maxSnowPosition.x):
                    snowParticle.setX(self.maxSnowPosition.x)
                    #use a negative vector:
                    currentDriftForce.x=self.driftForceRangeXYA.x
                    currentDriftRateOfChange.x=self.driftForceRateChangeRangeXYA.x
                    useXVector=currentDriftForce.x+currentDriftRateOfChange.x
                    snowParticle.setX(snowParticle,useXVector*globalClock.getDt())
                    snowParticle.setPythonTag("currentXYDriftForce",currentDriftForce)
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",currentDriftRateOfChange)
                if(snowParticle.getY()<self.minSnowPosition.y):
                    snowParticle.setY(self.minSnowPosition.y)
                    #use a positive vector:
                    currentDriftForce.y=self.driftForceRangeXYB.y
                    currentDriftRateOfChange.y=self.driftForceRateChangeRangeXYB.y
                    useYVector=currentDriftForce.y+currentDriftRateOfChange.y
                    snowParticle.setY(snowParticle,useYVector*globalClock.getDt())
                    snowParticle.setPythonTag("currentXYDriftForce",currentDriftForce)
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",currentDriftRateOfChange)
                if(snowParticle.getY()>self.maxSnowPosition.y):
                    snowParticle.setY(self.maxSnowPosition.y)
                    #use a negative vector:
                    currentDriftForce.y=self.driftForceRangeXYA.y
                    currentDriftRateOfChange.y=self.driftForceRateChangeRangeXYA.y
                    useYVector=currentDriftForce.y+currentDriftRateOfChange.y
                    snowParticle.setY(snowParticle,useYVector*globalClock.getDt())
                    snowParticle.setPythonTag("currentXYDriftForce",currentDriftForce)
                    snowParticle.setPythonTag("currentXYDriftForceRateChange",currentDriftRateOfChange)
        return task.cont
    
    #3.Lastly, apply some gravity to it, this also implements the kill-cycle:
    def taskToDropAndKillSnow(self,task):
        self.generalHandlerQueue.sortEntries()
        gotHitters=[]
        for i in range(self.generalHandlerQueue.getNumEntries()):
             entry=self.generalHandlerQueue.getEntry(i)
             intoNp=entry.getIntoNodePath()
             fromNp=entry.getFromNodePath()
             gotZ=entry.getSurfacePoint(render).getZ()
             actualSnowParticle=fromNp.getParent()
             if(actualSnowParticle not in gotHitters):
                 gotHitters.extend([actualSnowParticle,gotZ])
             else:
                 gotIndex=gotHitters.index(actualSnowParticle)+1
                 if(gotHitters[gotIndex]<gotZ):
                     gotHitters[gotIndex]=gotZ
        for i in range(0,len(gotHitters),2):
             actualSnowParticle=gotHitters[i]
             gotZ=gotHitters[i+1]
             #pull it down:
             if(actualSnowParticle.getZ(render)>gotZ):
                 snowGravityAcceleration=actualSnowParticle.getPythonTag("setAcceleration")
                 actualSnowParticle.setZ(actualSnowParticle,-snowGravityAcceleration * globalClock.getDt())
             elif(not actualSnowParticle.hasPythonTag("hitGround")):
                 actualSnowParticle.setPythonTag("hitGround",globalClock.getFrameTime())
             else:
                 timeDiff=globalClock.getFrameTime()-actualSnowParticle.getPythonTag("hitGround")
                 if(timeDiff>self.snowParticleRestTime):
                     actualSnowParticle.removeNode()
                     self.numSpawnedParticles-=1
        return task.cont
    #--End block.
    
    def spawnASnowParticle(self):
        array = GeomVertexArrayFormat()
        array.addColumn(InternalName.make('vertex'), 3,Geom.NTFloat32, Geom.CPoint)
        array.addColumn(InternalName.make('texcoord'), 2,Geom.NTFloat32, Geom.CTexcoord)
        array.addColumn(InternalName.make('normal'), 3,Geom.NTFloat32, Geom.CNormal)
        array.addColumn(InternalName.make('color'), 4,Geom.NTFloat32, Geom.CColor)
        format = GeomVertexFormat()
        format.addArray(array)
        format = GeomVertexFormat.registerFormat(format)
        node = GeomNode("ASnowFlakeLolz")
        #the writers and geom and primitive:
        vdata = GeomVertexData('VertexData', format, Geom.UHStatic)
        self.PSSTVertex = GeomVertexWriter(vdata, 'vertex')
        self.PSSTNormal = GeomVertexWriter(vdata, 'normal')
        self.PSSTColor = GeomVertexWriter(vdata, 'color')
        self.PSSTTexcoord = GeomVertexWriter(vdata, 'texcoord')
        tileGeom=Geom(vdata)
        tileGeom.setBoundsType (3)
        self.PSSTPrim = GeomTriangles(Geom.UHStatic)
        counter=0
        snowBody=[0,self.snowParticleDimension,0,self.snowParticleDimension]
        snowColour=LPoint4f(1,1,1,1)
        self.drawSnowParticle(snowBody,snowColour,counter)
        self.PSSTPrim.closePrimitive()
        tileGeom.addPrimitive(self.PSSTPrim)
        node.addGeom(tileGeom)
        gotProcGeom = render.attachNewNode(node)
        gotProcGeom.setName("snowParticleNumber_"+str(gotProcGeom.node().this))
        #gotProcGeom.setTwoSided(True)
        gotProcGeom.setCollideMask(BitMask32.allOff())
        #attach a collisionRay to it:
        raygeometry =CollisionRay(self.snowParticleDimension/2, 0, 5, 0, 0, -1)
        groundMask=BitMask32.bit(1)
        snowRay = gotProcGeom.attachNewNode(CollisionNode('NPCavatarRay'))
        snowRay.node().addSolid(raygeometry)
        snowRay.hide()
        #The masks:
        snowRay.node().setIntoCollideMask(BitMask32.allOff())
        snowRay.node().setFromCollideMask(groundMask)
        #add it to the traverser:
        self.traverser.addCollider(snowRay, self.generalHandlerQueue)
        
        #Lastly, each particle needs to have its own drift-force settings, stored via python-tags:
        startDriftX=random.uniform(self.driftForceRangeXYA.x,self.driftForceRangeXYB.x)
        startDriftY=random.uniform(self.driftForceRangeXYA.y,self.driftForceRangeXYB.y)
        
        startDriftXRateChange=random.uniform(self.driftForceRateChangeRangeXYA.x,self.driftForceRateChangeRangeXYB.x)
        startDriftYRateChange=random.uniform(self.driftForceRateChangeRangeXYA.y,self.driftForceRateChangeRangeXYB.y)
        
        gotProcGeom.setPythonTag("currentXYDriftForce",Point2(startDriftX,startDriftY))
        gotProcGeom.setPythonTag("currentXYDriftForceRateChange",Point2(startDriftXRateChange,startDriftYRateChange))
        #also, set the gravity acceleration for it:
        setAcceleration=random.uniform(9,15)
        gotProcGeom.setPythonTag("setAcceleration",setAcceleration)
        gotProcGeom.setBillboardPointWorld()
        return gotProcGeom
        
        
    
    def drawSnowParticle(self,sentStartEnd,sentColour,counter):
        #sentStartEnd->[xMin,xMax,zMin,zMax]
        #sentColour->[r,g,b,a]
        point1=Point3(sentStartEnd[0],0,sentStartEnd[2])
        point2=Point3(sentStartEnd[1],0,sentStartEnd[2])
        point3=Point3(sentStartEnd[1],0,sentStartEnd[3])
        point4=Point3(sentStartEnd[0],0,sentStartEnd[3])
        currentArray=[point1,point2,point3,point4]
        self.drawAGenericFace(self.PSSTVertex,self.PSSTNormal,self.PSSTColor,self.PSSTTexcoord,self.PSSTPrim,counter,currentArray,sentColour)
    
    def spawnAGenericCube(self,cubeColour,startPoint):
        array = GeomVertexArrayFormat()
        array.addColumn(InternalName.make('vertex'), 3,Geom.NTFloat32, Geom.CPoint)
        array.addColumn(InternalName.make('texcoord'), 2,Geom.NTFloat32, Geom.CTexcoord)
        array.addColumn(InternalName.make('normal'), 3,Geom.NTFloat32, Geom.CNormal)
        array.addColumn(InternalName.make('color'), 4,Geom.NTFloat32, Geom.CColor)
        format = GeomVertexFormat()
        format.addArray(array)
        format = GeomVertexFormat.registerFormat(format)
        node = GeomNode("aCubeGeom")
        #the writers and geom and primitive:
        vdata = GeomVertexData('VertexData', format, Geom.UHStatic)
        vertex = GeomVertexWriter(vdata, 'vertex')
        normal = GeomVertexWriter(vdata, 'normal')
        color = GeomVertexWriter(vdata, 'color')
        texcoord = GeomVertexWriter(vdata, 'texcoord')
        tileGeom=Geom(vdata)
        tileGeom.setBoundsType (3)
        prim = GeomTriangles(Geom.UHStatic)
        counter=0
        genericCubeListPoints=[]
        genericCubeListPoints=self.generateCubeGeneric(startPoint,self.cubeDimensions,[])
        for currentArray in genericCubeListPoints:
            self.drawAGenericFace(vertex,normal,color,texcoord,prim,counter,currentArray,cubeColour)
            counter+=4
        prim.closePrimitive()
        tileGeom.addPrimitive(prim)
        node.addGeom(tileGeom)
        gotProcGeom = render.attachNewNode(node)
        gotProcGeom.setName("kyubu_"+str(gotProcGeom.node().this))
        gotProcGeom.setCollideMask(BitMask32.allOff())
        #add a collisionSolid to it:
        endX=startPoint.x+self.cubeDimensions.x
        endY=startPoint.y+self.cubeDimensions.y
        endZ=startPoint.z+self.cubeDimensions.z
        endPoint=Point3(endX, endY, endZ)
        boxSolid=CollisionBox(startPoint,endPoint)
        boxColliderA=gotProcGeom.attachNewNode(CollisionNode('boxCNODE'))
        boxColliderA.setName("boxCollider")
        boxColliderA.node().addSolid(boxSolid)
        boxColliderA.show()
        #The masks:
        boxColliderA.setCollideMask(BitMask32.allOff())
        groundMask=BitMask32.bit(1)
        boxColliderA.node().setIntoCollideMask(groundMask)
        boxColliderA.node().setFromCollideMask(BitMask32.allOff())
        
    
    def generateCubeGeneric(self,originPoint,cubeDimension,exemptFaces):
        genericCubeListPoints=[]
        for i in range(1,7,1):
            if (i not in exemptFaces):
                #draw it:
                gotArray=self.returnProperFaceCoordinates(originPoint,cubeDimension,i)
                genericCubeListPoints.append(gotArray)
        return genericCubeListPoints
    
    def returnProperFaceCoordinates(self,*args):
        #0->(xOrigin,yOrigin,zOrigin)
        #1->(xDimension,yDimension,zDimension)
        #2->side to draw: 1,2,3,4,5,6: front,back,left,right,top,bottom
        originPoint=args[0]
        dimensionData=args[1]
        sideToDraw=args[2]
        if(sideToDraw==1):
            #drawing the front:
            point1=Point3(originPoint.x,originPoint.y,originPoint.z)
            point2=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z)
            point3=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z+dimensionData.z)
            point4=Point3(originPoint.x,originPoint.y,originPoint.z+dimensionData.z)
        elif(sideToDraw==2):
            #drawing the back:
            point1=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z)
            point2=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z)
            point3=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
            point4=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
        elif(sideToDraw==3):
            #drawing the left:
            point1=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z)
            point2=Point3(originPoint.x,originPoint.y,originPoint.z)
            point3=Point3(originPoint.x,originPoint.y,originPoint.z+dimensionData.z)
            point4=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
        elif(sideToDraw==4):
            #drawing the right:
            point1=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z)
            point2=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z)
            point3=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
            point4=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z+dimensionData.z)
        elif(sideToDraw==5):
            #drawing the top:
            point1=Point3(originPoint.x,originPoint.y,originPoint.z+dimensionData.z)
            point2=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z+dimensionData.z)
            point3=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
            point4=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z+dimensionData.z)
        elif(sideToDraw==6):
            #drawing the bottom:
            point1=Point3(originPoint.x,originPoint.y+dimensionData.y,originPoint.z)
            point2=Point3(originPoint.x+dimensionData.x,originPoint.y+dimensionData.y,originPoint.z)
            point3=Point3(originPoint.x+dimensionData.x,originPoint.y,originPoint.z)
            point4=Point3(originPoint.x,originPoint.y,originPoint.z)
        return [point1,point2,point3,point4]
        
    def drawAGenericFace(self,*args):
        #structure is: 
        #0->positional data.
        #1->normal data.
        #2->color data.
        #3->uv data.
        #4->primitive.
        #5->current starting index for primitive.
        #6->face structure.
        #7->optional colour setting.
        vertex=args[0]
        normal=args[1]
        color=args[2]
        texcoord=args[3]
        prim_dat=args[4]
        numbr=args[5]
        currentArray=args[6]
        for specificPoint in currentArray:
            vertex.addData3f(specificPoint.x,specificPoint.y,specificPoint.z)
            if(len(args)>7):
                colourData=args[7]
            else:
                colourData=LPoint4f(1,1,1,1)
            color.addData4f(colourData.x,colourData.y,colourData.z,colourData.w)
            texcoord.addData2f(1, 1)
        prim_dat.addVertices(numbr, numbr+1, numbr+2)
        prim_dat.addVertices(numbr, numbr+2, numbr+3)
   
runMe=run_me()
runMe.run()

I understand you’re just starting out so sorry to bombard you with this, but to break it down to the concepts used:

  • Each individual particle needs to be as plain and basic as possible, in this case, it’s a flat plane. You could add a simple texture with an alpha channel if you want it to look more like a snow particle.
  • A collisionRay is attached to each particle, this ray will intersect with other collisionSolids, such as a collisionBox or a collisionPolygon. When it hits one of these collisionSolids, we can get the z coordinate of the surface point that it hits.
  • Periodically, the x and y coordinates of each particle is changed to give it a sense of drift, just like snow behaves.
  • The z coordinate of each particle is periodically reduced due to gravity and it is here that the collisionRay proves most useful, since this reduction goes on until z coordinate of the particle matches the z coordinate of the surface point that the collisionRay hits.
  • Once a particle hits the ground, it is destroyed after a certain period of time has elapsed.

That’s basically it! I don’t know if the code will end up helping you, but you can do with it whatever you want. Just copy, paste and execute and tell me how it goes. This is something basic that can obviously be improved upon greatly, but as you can see, the framerate is pretty decent, staying at 59fps for 200 particles.

Best Regards.

2 Likes

@Thaumaturge, @Game_Starter - at the outset, thank you very much for your answers and I am sorry that I did not speak for so long, but I am currently overloaded with other duties.
In fact, I understand that Panda3D’s built-in Particles system does not have a lot of functionality and I would probably have to write my own system from scratch to get the fullness of what I would like to have. The example that @Game_Starter showed actually works functionally well, but unfortunately on the other hand its performance is far from what I want to achieve. Because whatever critical we say about the particles system built into Panda3D, it undoubtedly defends itself with the fact that it is relatively fast. The example from @Game_Starter runs smoothly (60 fps) for about 200 particles, but when I increased them to about 300, it did not cope with efficiency. Meanwhile, the system built into Panda3D easily manages to operate on much larger orders of magnitude of the number of partciles. For example, check out (working at 60fps) stylized rain effect (yes, I know, rain in the room doesn’t make sense, but it’s just an example), made of 10,000 particles (and I could probably run more, but 10,000 is more than enough):


Of course, the above doesn’t have any Collision Solids support - it’s just that these particles have a lifetime set so short that they disappear before they reach the ground.
And I know I could probably make my own particles with Collision Solids support, which would be efficient, but I would have to write more low-level code for that (C/C++ on CPU or some shader on GPU). But, since I assumed that I generally don’t want to go beyond Python (I just don’t know much enough about C/C++ or shader languages), I just have to accept what Panda3D offers and construct scenes so that I don’t have to have Collision Solids for particles.
Anyway, thanks for all the help and explanations.

It’s not a problem! And indeed, I can understand being occupied with other matters, I do believe! :slight_smile:

As to your example, in the case of rain, at least, you might be able to employ a workaround: you could create “splash” particles that appear on the surfaces, but that are overall unrelated to the actual “rain” particles. This might then give the impression that the rain is “hitting” the surface, without actually requiring that it be capable of doing so.

Now, this likely won’t work for all situations.However, it might nevertheless be worth looking for such workarounds, in case there is indeed some way of achieving an effect that emulates your intent.

1 Like

No problem! Good luck with your different approaches. The example I gave would obviously slow down if you used way too many particles, since it has to do collision tests in a loop and python is assiduously slow when it comes to doing things within loops. However, shifting to c++ might give significant speed increases as you noted and Panda3D makes it easy to mix c++ and python code as well. In case you want to go the c++ route in the future, let me know and I’ll try to help you in that regard too.

All the best.

1 Like

I’m digging up this old topic again.
At that time, I came to terms with the idea that simple individual particles (such as points, lines, or sprites) are not fully fledged objects in the Panda3D world, so they cannot interact with other objects, including collisions.
But I figured maybe I would outsmart the system if my particles were geomes with collision spheres attached.
So I took the “Pusher Example” code:
https://docs.panda3d.org/1.10/python/programming/collision-detection/pusher-example
In this example code, we have a stationary yellow “smiley” ball that is approached by a blue “frowney” but orbits when it touches.
I added a simple system particles to this code, which each particle renders as a blue “frowney” ball with an attached collision sphere (taken from the “Pusher Example”):

particles.renderer.setGeomNode(frowney.node())

And unfortunately - a failure again.
While the blue “frowney” ball, when moved as an independent object, correctly interacts with the yellow “smiley”, the same blue “frowney”, rendered as a particle, completely misses (passes through) the yellow ball “smiley”.


Does this mean that even when we render particles with the help of GeomParticleRenderer, such geomes still do not become fully fledged objects in Panda3D? Or maybe this idea is promising, and only I am doing something wrong?

The particle system to my knowledge is not built with the ability to simulate any kind of real-time physics-based collisions. For what you’d want to achieve, you’d have to use one of the three physics engines Panda3D supports, you can read more about them here. Though the particle system is connected to Panda3D’s in built physics engine and if you want to simulate physics forces using it, you’d have to use the PhysicsCollisionHandler and not the CollisionHandlerPusher to deal with colliding bodies using it.

1 Like

Thank you for your response. However, I do not know exactly how to interpret it.
As I understand it, I need to use the PhysicsCollisionHandler and not the CollisionHandlerPusher. The question, however, is how?
I tried a simple attempt to change the class of the pusher object:

pusher = PhysicsCollisionHandler()

Of course, it didn’t work:

Assertion failed: target.node()->is_of_type(ActorNode::get_class_type()) at line 199 of panda/src/physics/physicsCollisionHandler.cxx
Assertion failed: validate_target(target) at line 118 of panda/src/collide/collisionHandlerPhysical.cxx
Traceback (most recent call last):
  File "/Users/miklesz/PycharmProjects/Demo2023/collision.py", line 72, in <module>
    pusher.addCollider(frowneyC, frowney, base.drive.node())
AssertionError: target.node()->is_of_type(ActorNode::get_class_type()) at line 199 of panda/src/physics/physicsCollisionHandler.cxx

Unfortunately, the documentation in this regard is very poor. I did not find an example of using PhysicsCollisionHandler with particles in Panda3D.

The PhysicsCollisionHandler is only for use with the built-in physics system. As such, it is complaining that you’re not passing an ActorNode to the call.

1 Like

Thanks for all the answers.
I analyzed it all a bit, tried it a bit, and in general, I came to the conclusion that in general, in my opinion, the object collision detection only makes sense when in my application the movement of at least one of the objects would be non-deterministic (i.e., for example, the object would be controlled by a player). On the other hand, in a situation where the movement of objects is deterministic, it is much easier computationally to recalculate non-conflicting trajectories of objects in advance and move these objects in a pre-programmed manner in real time.

1 Like