Drag Selecting multiple nodes

I’m playing around at making an RTS. One of the things you need is a way to drag a box and select units. In that process you need a way of getting the nodes within that box. You could probably generate a collision solid on the fly to test for collisions within that boundary but I think it’s faster to just find the 2d relative point of the nodepaths and compare that to the mouse coordinates at the corners of the selection box. To do this I wrote the following function:

enjoy responsibly

	def Is3dpointIn2dRegion(self, node, point1,point2,point3d):
		"""This function takes a 2d selection box from the screen as defined by two corners
		and queries whether a given 3d point lies in that selection box
		Returns True if it is
		Returns False if it is not"""
		#node is the parent node- probably render or similar node
		#point1 is the first 2d coordinate in the selection box
		#point2 is the opposite corner 2d coordinate in the selection box
		#point3d is the point in 3d space to test if that point lies in the 2d selection box
		
		# Convert the point to the 3-d space of the camera 
		p3 = base.cam.getRelativePoint(node, point3d) 

		# Convert it through the lens to render2d coordinates 
		p2 = Point2() 
		if not base.camLens.project(p3, p2): 
			return False 
		
		r2d = Point3(p2[0], 0, p2[1]) 

		# And then convert it to aspect2d coordinates 
		a2d = aspect2d.getRelativePoint(render2d, r2d) 
		
		#Find out the biggest/smallest X and Y of the 2- 2d points provided.
		if point1.getX() > point2.getX():
			bigX = point1.getX()
			smallX = point2.getX()
		else:
			bigX = point2.getX()
			smallX = point1.getX()
			
		if point1.getY() > point2.getY():
			bigY = point1.getY()
			smallY = point2.getY()
		else:
			bigY = point2.getY()
			smallY = point2.getY()
		
		pX = a2d.getX()
		pY = a2d.getZ()  #aspect2d is based on a point3 not a point2 like render2d.
		
		if pX < bigX and pX > smallX:
			if pY < bigY and pY > smallY:
				
				return True
			else: return False
		else: return False

You provide the two mouse coordinates, the 3d point you’re testing, and it tells you if that 3d point is within the box coordinates.

I’m using this function to record the mouse points.

def savemousePos(self):
	pos = Point2(base.mouseWatcherNode.getMouse())
	pos.setX(pos.getX()*1.33)
	return pos

The multiplication of the X pos by 1.33 corrects between mouse coordinates (1x1 screen) to the actual screen dimensions that aspect2d uses (1.33x1 == 4x3).

Wow, thats really useful! Thanks for sharing!

:question: New to both python and panda but really enterested in your code
and this mite be a noob question but how can I get it to work
I’m interested in programing my own RTS and yours is the only
thing that comes close I try running it but all i get is

   File "RTS.py", line 1
   def Is3dpointIn2dRegion(self, node, point1,point2,point3d): 
   ^
   SyntaxError: invalid syntax

   I'm sure its something simple but how about helping out a noob

his code is part of a class, it does not look like yours is. Try some thing simple first to get a feel for the language.

Sorry I haven’t posted in a while. I’m a medical student and I have a huge test in June that I’m studying for right now.

@dogface69:

Treeform is right. This is just a function that does a very specific thing- it tells you if a 3d point (like the position of a node) is currently within a given 2d box on the screen. That’s all it does.

The reason that it is useful is that you can use the mouse to drag and create a box and then send the information to this function and ask if a certain node is in that box. If it is it will tell you so - which in my case lets you mark that object as being ‘selected’ so that your next actions will occur with reference to these selected objects.

In my program I have a class called RTSCamMouse which handles movement of my camera (based on the mouse being at the edge of the screen or zooming via the scroll wheel), and interprets all mouse clicks.

There is a separate class for Units, Buildings, Projectiles, and a master class that actually runs and referees everything.

I second Treeform’s advice- start with something simple like a box that you move around with the keyboard or something like that.

See my panda3dprojects profile for examples of some of my work- it’s all open source- free to edit and cannibalize. As for my RTS program- I may decide to market this specific program someday as it is medically based and designed as a teaching tool- so I want to retain this code- if you have specific questions I can help.

I experimented with this code, but wanted to come up with a drag-select that would allow you to only select part of an object. Using your code, one would have to create a bounding box for each object. I managed to do it with 4 CollisionPlanes. The code below is an example. It’s not the most organized, but it works and I tried to explain how.

class World(DirectObject):
  
  def __init__(self):
    for i in range(10):
      ball = loader.loadModel("smiley")
      ball.reparentTo(render)
      ball.setTag('click', '1')
      ball.setPos(random.randint(-4,4),random.randint(-4,4),random.randint(-4,4))
      cs = CollisionSphere(0, 0, 0, 1)
      cnodePath = ball.attachNewNode(CollisionNode('cnode'))
      cnodePath.node().addSolid(cs)
      cnodePath.node().setIntoCollideMask(BitMask32.allOff())
    self.accept("mouse1", self.click)
    self.accept("mouse1-up", self.release_click)
    
  def click(self):
    if not base.mouseWatcherNode.hasMouse():
      return
    self.click_start = Point2(base.mouseWatcherNode.getMouseX(),base.mouseWatcherNode.getMouseY())
    cmfg = CardMaker('fg')
    cmfg.setFrame(0,4/3,-1,0)
    self.bar = aspect2d.attachNewNode(cmfg.generate())
    self.bar.setPos(self.click_start[0]*4/3,1,self.click_start[1])
    self.bar.setColor(0,1,0,.5)
    self.bar.setTransparency(1)
    taskMgr.add(w.update_rect, "update_rect")
  def update_rect(self, task):
    if not base.mouseWatcherNode.hasMouse():  #check for mouse first, in case the mouse is outside the Panda window
      return Task.cont
    x = base.mouseWatcherNode.getMouseX()  #get the current mouse position
    y = base.mouseWatcherNode.getMouseY()
    self.bar.setScale((x - self.click_start[0])*4/3, 1, self.click_start[1] - y)  #X is scaled because of the different aspect ratios (1:1 vs 3:4)
    return Task.cont
  def release_click(self):
    taskMgr.remove("update_rect")
    self.bar.removeNode()
    if not base.mouseWatcherNode.hasMouse():
      return
    self.click_end = Point2(base.mouseWatcherNode.getMouseX(),base.mouseWatcherNode.getMouseY())
    if self.click_end == self.click_start:  #Fudge the numbers a bit to avoid the degenerate case of no rectangle
      self.click_end = Point2(self.click_end[0]+.00001, self.click_end[1]+.00001)
      #I would have used the simpler PickerRay method, but the collision masks on the objects are reversed
    pt = Point3()
    junk_pt = Point3()
    base.camLens.extrude(self.click_start, junk_pt, pt)  #calculate a point in 3d space from the mouse position
    click_start3 = pt  #Use the point farthest away from the camera for the best precision
    pt2 = Point3()
    base.camLens.extrude(self.click_end, junk_pt, pt2)
    click_end3 = pt2
    # NE <-> SW drag has the planes flipped compared to NW <-> SE
    if (self.click_end[0] < self.click_start[0] and self.click_end[1] < self.click_start[1]) or (self.click_end[0] > self.click_start[0] and self.click_end[1] > self.click_start[1]):
      left = CollisionPlane(Plane(camera.getPos(),Point3(click_start3[0],1000,click_end3[2]), click_start3 )) #Create the 4 sides with planes
      right=CollisionPlane(Plane(camera.getPos(),Point3(click_end3[0],1000,click_start3[2]),click_end3 )) #They should all 'face' inward: i.e.
      bot = CollisionPlane(Plane(camera.getPos(), click_end3, Point3(click_start3[0],1000,click_end3[2])))  #Collisions are inside
      top = CollisionPlane(Plane(camera.getPos(), click_start3, Point3(click_end3[0],1000,click_start3[2])))
    else:
      left = CollisionPlane(Plane(camera.getPos(),click_start3, Point3(click_start3[0],1000,click_end3[2])))  #Changing the order of the points defining
      right=CollisionPlane(Plane(camera.getPos(),click_end3, Point3(click_end3[0],1000,click_start3[2])))  #the plane flips its direction
      bot = CollisionPlane(Plane(camera.getPos(), Point3(click_start3[0],1000,click_end3[2]),click_end3 ))
      top = CollisionPlane(Plane(camera.getPos(), Point3(click_end3[0],1000,click_start3[2]),click_start3 ))
    pyramid = render.attachNewNode(CollisionNode('pyramid'))
    pyramid.node().addSolid(left)
    pyramid.node().addSolid(top)
    pyramid.node().addSolid(right)
    pyramid.node().addSolid(bot)
    click_traverser = CollisionTraverser('click_enabler')
    click_handler = CollisionHandlerQueue()
    list = render.findAllMatches("=click")  #Find all clickable items
    for i in range(list.getNumPaths()):
      click_traverser.addCollider(list[i].find("+CollisionNode"), click_handler)
    click_traverser.traverse(render)
    hits = []
    for i in range(click_handler.getNumEntries()):
      hits.append(click_handler.getEntry(i).getFromNodePath().getParent())  #this actually includes all clickable items since the planes cover
                                                                                                                       #everything.  However, some will be listed more than once
    for i in hits:
      if hits.count(i) == 4:  #If the object is within the rectangle, it collides with all 4 planes
        i.setColor(random.random(),random.random(),random.random())
    click_traverser.clearColliders()
    click_handler.clearEntries()
    pyramid.removeNode()

w = World()
run()

EDIT: Fixed bug when dragging NE <-> SW

I filled it to have a standalone working sample

*see downward*

That’s still not right, since each selected object is processed 4 times.

The correct and a bit cleaned up one :

scroll down
  1. moved the selection rectangle to render2d, to avoid involving aspect ratio
  2. start & end points are always lower left and upper right corners, to avoid conditional branching for creating the collision planes
  3. click up event is caught after the click, not always, so on Windows, when the window loses focus, the up event won’t be generated, which causes an error since “bar” attribute is created after the 1st click

nice cleanup Jo!
by the way I confirm the off-window issue also on Kubuntu Linux
also I see often from console these kind of non-blocking assertions:

:linmath(warning): Tried to invert singular LMatrix3.

anyhow now is a nice snip to start with

That happens when something gets scaled to 0. You’d need to adapt the code that it checks if the drag area size is 0, it doesn’t get scaled.

  def update_rect(self, task):
    if not base.mouseWatcherNode.hasMouse():  #check for mouse first, in case the mouse is outside the Panda window
      return Task.cont
    sx = base.mouseWatcherNode.getMouseX() - self.click_start[0]
    sy = self.click_start[1] - base.mouseWatcherNode.getMouseY()
    self.bar.setScale(sx if sx else .0001, 1, sy if sy else .0001)
    return Task.cont

I solved the off-window issue so we gotta a final release I guess

''' 
by: mavasher
revisers: crossedgun, ynjh_jo, pro_rsoft, astelix
'''
from pandac.PandaModules import *
from direct.directbase.DirectStart import *
from direct.showbase.DirectObject import DirectObject
from direct.task import Task
import random

class World(DirectObject):

  def __init__(self):
    for i in range(10):
      ball = loader.loadModel("smiley")
      ball.reparentTo(render)
      ball.setTag('click', '1')
      ball.setPos(random.randint(-4,4),random.randint(-4,4),random.randint(-4,4))
      cs = CollisionSphere(0, 0, 0, 1)
      cnodePath = ball.attachNewNode(CollisionNode('cnode'))
      cnodePath.node().addSolid(cs)
      cnodePath.node().setIntoCollideMask(BitMask32.allOff())
    self.accept("mouse1", self.click)
  #-----------------------------------------
  def click(self):
    if not base.mouseWatcherNode.hasMouse():
      return
    self.click_start = Point2(base.mouseWatcherNode.getMouseX(),base.mouseWatcherNode.getMouseY())
    cmfg = CardMaker('fg')
    cmfg.setFrame(0, 1, -1, 0)
    self.bar = render2d.attachNewNode(cmfg.generate())
    self.bar.setPos(self.click_start[0],1,self.click_start[1])
    self.bar.setColor(0,1,0,.5)
    self.bar.setTransparency(1)
    taskMgr.add(w.update_rect, "update_rect")
    self.acceptOnce("mouse1-up", self.release_click)
  #-----------------------------------------
  def update_rect(self, task):
    if not base.mouseWatcherNode.hasMouse():  #check for mouse first, in case the mouse is outside the Panda window
      return Task.cont
    sx = base.mouseWatcherNode.getMouseX() - self.click_start[0]
    sy = self.click_start[1] - base.mouseWatcherNode.getMouseY()
    self.bar.setScale(sx if sx else .0001, 1, sy if sy else .0001)
    return Task.cont
  #-----------------------------------------
  def release_click(self):
    taskMgr.remove("update_rect")
    
    click_start=Point2(self.bar.getTightBounds()[0][0], self.bar.getTightBounds()[0][2])
    click_end=Point2(self.bar.getTightBounds()[1][0], self.bar.getTightBounds()[1][2])
    self.bar.removeNode()
    
    if click_end == click_start:  #Fudge the numbers a bit to avoid the degenerate case of no rectangle
      click_end = Point2(click_end[0]+.00001, click_end[1]+.00001)
      #I would have used the simpler PickerRay method, but the collision masks on the objects are reversed
    pt = Point3()
    junk_pt = Point3()
    base.camLens.extrude(click_start, junk_pt, pt)  #calculate a point in 3d space from the mouse position
    click_start3 = pt  #Use the point farthest away from the camera for the best precision
    click_end3 = Point3()
    base.camLens.extrude(click_end, junk_pt, click_end3)
    left = CollisionPlane(Plane(camera.getPos(),Point3(click_start3[0],1000,click_end3[2]), click_start3 )) #Create the 4 sides with planes
    right=CollisionPlane(Plane(camera.getPos(),Point3(click_end3[0],1000,click_start3[2]),click_end3 )) #They should all 'face' inward: i.e.
    bot = CollisionPlane(Plane(camera.getPos(), click_end3, Point3(click_start3[0],1000,click_end3[2])))  #Collisions are inside
    top = CollisionPlane(Plane(camera.getPos(), click_start3, Point3(click_end3[0],1000,click_start3[2])))
    pyramid = render.attachNewNode(CollisionNode('pyramid'))
    pyramid.node().addSolid(left)
    pyramid.node().addSolid(top)
    pyramid.node().addSolid(right)
    pyramid.node().addSolid(bot)
    click_traverser = CollisionTraverser('click_enabler')
    click_handler = CollisionHandlerQueue()
    for ci in render.findAllMatches("=click").asList():
      click_traverser.addCollider(ci.find("+CollisionNode"), click_handler)
    click_traverser.traverse(render)
    hits = []
    for i in range(click_handler.getNumEntries()):
      hits.append(click_handler.getEntry(i).getFromNodePath().getParent())  #this actually includes all clickable items since the planes cover everything.

    # If the object is within the rectangle, it collides with all 4 planes
    # Use set to remove 3 duplicates of each selected object
    sel=set(filter(lambda i: hits.count(i) == 4, hits))
    print len(sel),'selected'

    all=render.findAllMatches("**/smiley*")
    for i in range(all.getNumPaths()): all[i].clearColor()

    for i in sel:
      i.setColor(random.random(),random.random(),random.random())
    click_traverser.clearColliders()
    click_handler.clearEntries()
    pyramid.removeNode()

camera.setPos(0, -25, 5)
base.disableMouse()
w = World()
run()

That’s good, but shouldn’t the selection bounds be clamped to -1…1, so the offscreen objects won’t be selected ?? Or is that way better ??

well it is just a snippet to start with - personally I guess it does not make sense to waste time to make it perfect but obviously I guess that nobody would complain if it would be perfect :wink:

Hello,

I am in need of a RTS 3D selection box implementation, and was excited to see an implementation available.

However, I implemented the at the code provided above but I am not getting the behavior I was anticipating. First I noticed several comments about behavior being different for different platforms so I am running OSX.5 with Panda3d 1.6.2.

On my platform, it seems the single mouse click selection is erratic where it “appears” selects a sphere randomly, not by spheres located near or under the mouse click. Also the pyramid selection has similar behavior.

I have noticed the algorithm above comments about CollisionPlanes intersecting with items in front of the plane rather than objects behind the plane. This is opposite of the information provided in the Panda3D manual at panda3d.org/manual/index.php/Collision_Solids

I thought I might check to see if anyone as experienced similar behavior.
thanks

Someone else can correct me if I’m wrong here, but I think the simple version is just the original code snippet. The more advanced code that was posted later was created with the intent of selecting parts of nodes but not necessarily the center of the node. That requires collision. The simple version I originally posted is just a lens transform so I imagine it’s probably faster than the collision method but it has the limitation that the selection box will have to include the position of the node in the selection box, not just a part of the node. In practice, for an RTS game I’m not sure the collision method holds any real advantage, since the individual units of your RTS will be pretty small compared to the selection box- the players will simply have to draw their selection boxes such that they include the center of the unit they want to select. Also, I think the lens transform method I originally posted will be easier to set up in a new program. In my old RTS project I used selection boxes for multiple units and a collision ray for selecting single units.

I guess nobody noticed that before because the camera is static and positioned not far from origin, so the deviation is not obvious.
Plus, the plane’s 3 points are in camera’s coordinate system, while the pyramid is parented to render, that’s also a fatal mistake.

I have fixed it :

from pandac.PandaModules import *
from direct.directbase.DirectStart import *
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.task import Task
import random, sys

class World(DirectObject):

  def __init__(self):
    self.collTrav = CollisionTraverser('selector')
    self.selHandler = CollisionHandlerQueue()
    self.selection = set()
    CM = CardMaker('sel')
    CM.setFrame(0,1,0,1)
    self.rect = render2d.attachNewNode(CM.generate())
    self.rect.setColor(0,1,1,.2)
    self.rect.setTransparency(1)
    self.rect.hide()
    LS = LineSegs()
    LS.moveTo(0,0,0)
    LS.drawTo(1,0,0)
    LS.drawTo(1,0,1)
    LS.drawTo(0,0,1)
    LS.drawTo(0,0,0)
    self.rect.attachNewNode(LS.create())

    for i in range(20):
      ball = loader.loadModel("smiley")
      ball.reparentTo(render)
      ball.setPos(random.randint(-3,3)*2,random.randint(-3,3)*2,random.randint(-3,3)*2)
      cnodePath = ball.attachCollisionSphere(
         'sel', 0,0,0, 1, BitMask32.bit(1), BitMask32.allOff() )
      self.collTrav.addCollider(cnodePath,self.selHandler)
      #~ ball.showCS()

    self.accept("escape", sys.exit)
    self.accept("mouse1", self.click)

  def click(self):
    if not base.mouseWatcherNode.hasMouse():
      return
    self.click_start = Point2(base.mouseWatcherNode.getMouse())
    self.rect.setPos(self.click_start[0],1,self.click_start[1])
    self.rect.show()
    t = taskMgr.add(self.update_rect, "update_rect")
    self.acceptOnce("mouse1-up", self.release_click, extraArgs=[t])
    for i in self.selection:
        i.setColorScale(Vec4(1))
    self.selection = set()

  def update_rect(self, task):
    if not base.mouseWatcherNode.hasMouse():  #check for mouse first, in case the mouse is outside the Panda window
      return Task.cont
    d = base.mouseWatcherNode.getMouse() - self.click_start
    self.rect.setScale(d[0] if d[0] else 1e-3, 1, d[1] if d[1] else 1e-3)
    return Task.cont

  def release_click(self,t):
    taskMgr.remove(t)
    bmin,bmax=self.rect.getTightBounds()
    clickLL=Point2(bmin[0], bmin[2]) # lower left
    clickUR=Point2(bmax[0], bmax[2]) # upper right
    if clickUR == clickLL:  #Fudge the numbers a bit to avoid the degenerate case of no rectangle
       clickUR = Point2(clickUR[0]+.00001, clickUR[1]+.00001)
    crap = Point3()
    llF=Point3()
    urF=Point3()
    base.camLens.extrude(clickLL, crap, llF)
    base.camLens.extrude(clickUR, crap, urF)

    ulF = Point3(llF[0],llF[1],urF[2])
    brF = Point3(urF[0],urF[1],llF[2])

    camOrigin=Point3(0)
    left = CollisionPlane( Plane(camOrigin, ulF, llF) ) #Create the 4 sides with planes
    right= CollisionPlane( Plane(camOrigin, brF, urF) ) #They should all 'face' OUTward: i.e.
    bot  = CollisionPlane( Plane(camOrigin, llF, brF) ) #Collisions are INSIDE
    top  = CollisionPlane( Plane(camOrigin, urF, ulF) )
    pyramid = camera.attachNewNode(CollisionNode('pyramid'))
    pyramid.node().addSolid(left)
    pyramid.node().addSolid(top)
    pyramid.node().addSolid(right)
    pyramid.node().addSolid(bot)

    # check for collisions
    self.collTrav.traverse(render)
    hits = []
    for i in range(self.selHandler.getNumEntries()):
      hits.append(self.selHandler.getEntry(i).getFromNodePath().getParent())

    # If the object is within the rectangle, it collides with all 4 planes.
    # Use set to remove duplicates collision entries of the same object.
    self.selection = set(filter(lambda i: hits.count(i) == 4, hits))
    for i in self.selection:
        i.setColorScale( Vec4(1,0,0,1))
    self.selHandler.clearEntries()
    pyramid.removeNode()
    self.rect.hide()
    print len(self.selection),'selected'

base.disableMouse()
# animate camera orientation, to see if the pyramid is correct
camPivot = render.attachNewNode('')
camera.reparentTo(camPivot)
camera.setY(-30)
pos=camera.getPos()
Sequence(
  camera.posInterval(20,pos*2.5,pos),
  camera.posInterval(20,pos),
  ).loop()
camPivot.hprInterval(40,Vec3(360)).loop()
winst = World()
run()

And this one is for dynamic selection as you drag (might be quite expensive since the collision traversal is performed every frame) :

from pandac.PandaModules import *
from direct.directbase.DirectStart import *
from direct.showbase.DirectObject import DirectObject
from direct.interval.IntervalGlobal import *
from direct.task import Task
import random, sys

class World(DirectObject):

  def __init__(self):
      self.collTrav = CollisionTraverser('selector')
      self.selHandler = CollisionHandlerQueue()
      self.selection = set()
      dummyPlane = Plane()
      self.leftP = CollisionPlane(dummyPlane)
      self.rightP = CollisionPlane(dummyPlane)
      self.bottomP = CollisionPlane(dummyPlane)
      self.topP = CollisionPlane(dummyPlane)
      self.pyramid = CollisionNode('pyramid')
      self.pyramid.addSolid(self.leftP)
      self.pyramid.addSolid(self.rightP)
      self.pyramid.addSolid(self.bottomP)
      self.pyramid.addSolid(self.topP)
      camera.attachNewNode(self.pyramid)
      self.llF = Point3()
      self.urF = Point3()
      # selection rectangle
      CM = CardMaker('')
      CM.setFrame(0,1,0,1)
      self.rect = render2d.attachNewNode(CM.generate())
      self.rect.setColor(1,1,0,.2)
      self.rect.setTransparency(1)
      self.rect.hide()
      LS = LineSegs()
      LS.moveTo(0,0,0)
      LS.drawTo(1,0,0)
      LS.drawTo(1,0,1)
      LS.drawTo(0,0,1)
      LS.drawTo(0,0,0)
      self.rect.attachNewNode(LS.create())

      # I don't want overlapping objects
      for z in range(5):
          for y in range(5):
              for x in range(5):
                  if random.randint(0,2)!=2: continue
                  ball = loader.loadModel("smiley")
                  ball.reparentTo(render)
                  ball.setPos(x*3,y*3,z*3)
                  ball.setHpr(random.random()*360,random.random()*360,random.random()*360)
                  cnodePath = ball.attachCollisionSphere(
                     '', 0,0,0, 1, BitMask32.bit(1), BitMask32.allOff() )
                  self.collTrav.addCollider(cnodePath,self.selHandler)
                  #~ ball.showCS() # show collision solids

      self.accept("escape", sys.exit)
      self.accept("mouse1", self.click)

  def click(self):
      if not base.mouseWatcherNode.hasMouse():
         return
      self.click_start = Point2(base.mouseWatcherNode.getMouse())
      self.rect.setPos(self.click_start[0],1,self.click_start[1])
      self.rect.show()
      t = taskMgr.add(self.update_rect, "update_rect")
      t.lastMpos = None
      self.acceptOnce("mouse1-up", self.release_click, extraArgs=[t])
      for i in self.selection:
          i.setColorScale(Vec4(1))
      self.selection = set()

  def update_rect(self, task):
      if not base.mouseWatcherNode.hasMouse():  #check for mouse first, in case the mouse is outside the Panda window
         return Task.cont
      mpos = base.mouseWatcherNode.getMouse()
      if task.lastMpos!=mpos: # updates the pyramid planes only on mouse move
         task.lastMpos = Point2(mpos)
         d = mpos - self.click_start
         self.rect.setScale(d[0] if d[0] else 1e-3, 1, d[1] if d[1] else 1e-3)

         bmin,bmax=self.rect.getTightBounds()
         clickLL=Point2(bmin[0], bmin[2]) # lower left
         if bmin==bmax:  #Fudge the numbers a bit to avoid the degenerate case of no rectangle
            clickUR = Point2(clickUR[0]+.00001, clickUR[1]+.00001)
         else:
            clickUR=Point2(bmax[0], bmax[2]) # upper right
         crap = Point3()
         base.camLens.extrude(clickLL, crap, self.llF)
         base.camLens.extrude(clickUR, crap, self.urF)

         ulF = Point3(self.llF[0],self.llF[1],self.urF[2])
         brF = Point3(self.urF[0],self.urF[1],self.llF[2])

         camOrigin=Point3(0)
         self.leftP.setPlane(   Plane(camOrigin, ulF, self.llF) ) #Create the 4 sides with planes
         self.rightP.setPlane(  Plane(camOrigin, brF, self.urF) ) #They should all 'face' OUTward: i.e.
         self.bottomP.setPlane( Plane(camOrigin, self.llF, brF) ) #Collisions are INSIDE
         self.topP.setPlane(    Plane(camOrigin, self.urF, ulF) )

      # checks for collisions
      self.collTrav.traverse(render)
      hits = []
      newSel = set()
      for i in range(self.selHandler.getNumEntries()):
          obj=self.selHandler.getEntry(i).getFromNodePath().getParent()
          numColl=hits.count(obj)
          # an object is within the rectangle, only if it collides with all 4 planes
          if numColl==3:
             newSel.add(obj)
          elif numColl<3:
             hits.append(obj)

      deSel = self.selection.difference(newSel)  # no longer selected objects
      newSel = newSel.difference(self.selection) # new selected objects
      for o in newSel:
          self.selection.add(o)
          o.setColorScale(0,1,0,1)
      for o in deSel:
          self.selection.remove(o)
          o.setColorScale(Vec4(1))
      return Task.cont

  def release_click(self,t):
      taskMgr.remove(t)
      print len(self.selection),'selected'
      self.rect.hide()

base.disableMouse()
# animates camera orientation, to prove that the pyramid is correct
camPivot = render.attachNewNode('')
camPivot.setPos(7,7,7)
camera.reparentTo(camPivot)
camera.setY(-30)
pos=camera.getPos()
Sequence(
  camera.posInterval(15,pos*2.5,pos),
  camera.posInterval(15,pos),
  ).loop()
camPivot.hprInterval(30,Vec3(360)).loop()
winst = World()
run()

In both scripts, the camera is continuously transformed, to prove that the pyramid is correctly built.

I did noticed but ain’t the skills nor the time to fix it
excellent job as always Jo!

hello, i read this post and it looks very helpful, I added it into the code for an RTS i’m starting to work on and the program runs without error, but i can’t seem to select the bulldozer model that i’m trying to select.

I’m not sure if there is something i need to do to the bulldozer node to make it so that it can work with your 3D selection function. maybe you can help me out a little bit with this…

here is the code for my program. (sorry if its a bit long and obtuse, i am just really starting to get a grasp on working with python and panda3D and i’m sure that i can make this code shorter somehow.

from direct.showbase.ShowBase import ShowBase,DirectObject
from pandac.PandaModules import GeoMipTerrain, Vec3,Vec2
import math

class WarGames3D(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        self.world = self.loader.loadModel("world2.bam")
        self.world.setPos(0,0,-200)
        self.world.reparentTo(self.render)       
        
        self.bulldozer = self.loader.loadModel("bulldozer.egg")
        self.bulldozer.setPos(50,50,110)
        self.bulldozer.setScale(20,20,20)
        tex = loader.loadTexture("clouds.png")
        self.bulldozer.setTexture(tex, 1)
        self.bulldozer.setH(33)
        self.bulldozer.reparentTo(self.world)
        
        #camera controls start here
        
        base.disableMouse() 
      # This disables the default mouse based camera control used by panda. This default control is awkward, and won't be used. 
       
        base.camera.setPos(0,20,20) 
        base.camera.lookAt(0,0,0) 
      # Gives the camera an initial position and rotation. 
       
        self.mx,self.my=0,0 
      # Sets up variables for storing the mouse coordinates 
       
        self.orbiting=False 
      # A boolean variable for specifying whether the camera is in orbiting mode. Orbiting mode refers to when the camera is being moved 
      # because the user is holding down the right mouse button. 
       
        self.target=Vec3() 
      # sets up a vector variable for the camera's target. The target will be the coordinates that the camera is currently focusing on. 
       
        self.camDist = 40 
      # A variable that will determine how far the camera is from it's target focus 
       
        self.panRateDivisor = 10 
      # This variable is used as a divisor when calculating how far to move the camera when panning. Higher numbers will yield slower panning 
      # and lower numbers will yield faster panning. This must not be set to 0. 
       
        self.panZoneSize = .27 
      # This variable controls how close the mouse cursor needs to be to the edge of the screen to start panning the camera. It must be less than 1, 
      # and I recommend keeping it less than .2 
       
        self.panLimitsX = Vec2(-2048, 2048) 
        self.panLimitsY = Vec2(-2048, 2048) 
      # These two vairables will serve as limits for how far the camera can pan, so you don't scroll away from the map. 

        self.setTarget(0,0,0) 
      # calls the setTarget function to set the current target position to the origin. 
       
        self.turnCameraAroundPoint(0,0) 
      # calls the turnCameraAroundPoint function with a turn amount of 0 to set the camera position based on the target and camera distance 
       
        self.accept("mouse3",self.startOrbit) 
      # sets up the camrea handler to accept a right mouse click and start the "drag" mode. 
       
        self.accept("mouse3-up",self.stopOrbit) 
      # sets up the camrea handler to understand when the right mouse button has been released, and ends the "drag" mode when 
      # the release is detected. 
       
      # The next pair of lines use lambda, which creates an on-the-spot one-shot function. 
       
        self.accept("wheel_up",lambda : self.adjustCamDist(0.9)) 
      # sets up the camera handler to detet when the mouse wheel is rolled upwards and uses a lambda function to call the 
      # adjustCamDist function  with the argument 0.9 
       
        self.accept("wheel_down",lambda : self.adjustCamDist(1.1)) 
      # sets up the camera handler to detet when the mouse wheel is rolled upwards and uses a lambda function to call the 
      # adjustCamDist function  with the argument 1.1 
       
        taskMgr.add(self.camMoveTask,'camMoveTask') 
      # sets the camMoveTask to be run every frame 
       
    def turnCameraAroundPoint(self,deltaX,deltaY): 
      # This function performs two important tasks. First, it is used for the camera orbital movement that occurs when the 
      # right mouse button is held down. It is also called with 0s for the rotation inputs to reposition the camera during the 
      # panning and zooming movements. 
      # The delta inputs represent the change in rotation of the camera, which is also used to determine how far the camera 
         # actually moves along the orbit. 
       
         newCamHpr = Vec3() 
         newCamPos = Vec3() 
         # Creates temporary containers for the new rotation and position values of the camera. 
          
         camHpr=base.camera.getHpr() 
         # Creates a container for the current HPR of the camera and stores those values. 
          
         newCamHpr.setX(camHpr.getX()+deltaX) 
         newCamHpr.setY(self.clamp(camHpr.getY()-deltaY, -85, -10)) 
         newCamHpr.setZ(camHpr.getZ()) 
         # Adjusts the newCamHpr values according to the inputs given to the function. The Y value is clamped to prevent 
         # the camera from orbiting beneath the ground plane and to prevent it from reaching the apex of the orbit, which 
         # can cause a disturbing fast-rotation glitch. 
          
         base.camera.setHpr(newCamHpr) 
         # Sets the camera's rotation to the new values. 
          
         angleradiansX = newCamHpr.getX() * (math.pi / 180.0) 
         angleradiansY = newCamHpr.getY() * (math.pi / 180.0) 
         # Generates values to be used in the math that will calculate the new position of the camera. 
          
         newCamPos.setX(self.camDist*math.sin(angleradiansX)*math.cos(angleradiansY)+self.target.getX())
         newCamPos.setY(-self.camDist*math.cos(angleradiansX)*math.cos(angleradiansY)+self.target.getY()) 
         newCamPos.setZ(-self.camDist*math.sin(angleradiansY)+self.target.getZ()) 
         base.camera.setPos(newCamPos.getX(),newCamPos.getY(),newCamPos.getZ()) 
         # Performs the actual math to calculate the camera's new position and sets the camera to that position. 
         #Unfortunately, this math is over my head, so I can't fully explain it. 
                      
         base.camera.lookAt(self.target.getX(),self.target.getY(),self.target.getZ() ) 
         # Points the camera at the target location. 
          
    def setTarget(self,x,y,z): 
      #This function is used to give the camera a new target position. 
      x = self.clamp(x, self.panLimitsX.getX(), self.panLimitsX.getY()) 
      self.target.setX(x) 
      y = self.clamp(y, self.panLimitsY.getX(), self.panLimitsY.getY()) 
      self.target.setY(y) 
      self.target.setZ(z) 
      # Stores the new target position values in the target variable. The x and y values are clamped to the pan limits. 
       
    def setPanLimits(self,xMin, xMax, yMin, yMax): 
      # This function is used to set the limitations of the panning movement. 
       
      self.panLimitsX = (xMin, xMax) 
      self.panLimitsY = (yMin, yMax) 
      # Sets the inputs into the limit variables. 
       
    def clamp(self, val, minVal, maxVal): 
      # This function constrains a value such that it is always within or equal to the minimum and maximum bounds. 
       
      val = min( max(val, minVal), maxVal) 
      # This line first finds the larger of the val or the minVal, and then compares that to the maxVal, taking the smaller. This ensures 
      # that the result you get will be the maxVal if val is higher than it, the minVal if val is lower than it, or the val itself if it's 
      # between the two. 
       
      return val 
      # returns the clamped value 
       
    def startOrbit(self): 
      # This function puts the camera into orbiting mode. 
       
      self.orbiting=True 
      # Sets the orbiting variable to true to designate orbiting mode as on. 
       
    def stopOrbit(self): 
      # This function takes the camera out of orbiting mode. 
       
      self.orbiting=False 
      # Sets the orbiting variable to false to designate orbiting mode as off. 
       
    def adjustCamDist(self,distFactor): 
      # This function increases or decreases the distance between the camera and the target position to simulate zooming in and out. 
      # The distFactor input controls the amount of camera movement. 
         # For example, inputing 0.9 will set the camera to 90% of it's previous distance. 
       
      self.camDist=self.camDist*distFactor 
      # Sets the new distance into self.camDist. 
       
      self.turnCameraAroundPoint(0,0) 
      # Calls turnCameraAroundPoint with 0s for the rotation to reset the camera to the new position. 
       
    def camMoveTask(self,task): 
      # This task is the camera handler's work house. It's set to be called every frame and will control both orbiting and panning the camera. 
       
      if base.mouseWatcherNode.hasMouse(): 
         # We're going to use the mouse, so we have to make sure it's in the game window. If it's not and we try to use it, we'll get 
         # a crash error. 
          
         mpos = base.mouseWatcherNode.getMouse() 
         # Gets the mouse position 
          
         if self.orbiting: 
         # Checks to see if the camera is in orbiting mode. Orbiting mode overrides panning, because it would be problematic if, while 
         # orbiting the camera the mouse came close to the screen edge and started panning the camera at the same time. 
          
            self.turnCameraAroundPoint((self.mx-mpos.getX())*100,(self.my-mpos.getY())*100)        
            # calculates new values for camera rotation based on the change in mouse position. mx and my are used here as the old 
            # mouse position. 
             
         else: 
         # If the camera isn't in orbiting mode, we check to see if the mouse is close enough to the edge of the screen to start panning 
          
            moveY=False 
            moveX=False 
            # these two booleans are used to denote if the camera needs to pan. X and Y refer to the mouse position that causes the 
            # panning. X is the left or right edge of the screen, Y is the top or bottom. 
             
            if self.my > (1 - self.panZoneSize): 
               angleradiansX1 = base.camera.getH() * (math.pi / 180.0) 
               panRate1 = (1 - self.my - self.panZoneSize) * (self.camDist / self.panRateDivisor) 
               moveY = True 
            if self.my < (-1 + self.panZoneSize): 
               angleradiansX1 = base.camera.getH() * (math.pi / 180.0)+math.pi 
               panRate1 = (1 + self.my - self.panZoneSize)*(self.camDist / self.panRateDivisor) 
               moveY = True 
            if self.mx > (1 - self.panZoneSize): 
               angleradiansX2 = base.camera.getH() * (math.pi / 180.0)+math.pi*0.5 
               panRate2 = (1 - self.mx - self.panZoneSize) * (self.camDist / self.panRateDivisor) 
               moveX = True 
            if self.mx < (-1 + self.panZoneSize): 
               angleradiansX2 = base.camera.getH() * (math.pi / 180.0)-math.pi*0.5 
               panRate2 = (1 + self.mx - self.panZoneSize) * (self.camDist / self.panRateDivisor) 
               moveX = True 
            # These four blocks check to see if the mouse cursor is close enough to the edge of the screen to start panning and then 
            # perform part of the math necessary to find the new camera position. Once again, the math is a bit above my head, so 
            # I can't properly explain it. These blocks also set the move booleans to true so that the next lines will move the camera. 
                
            if moveY: 
               tempX = self.target.getX()+math.sin(angleradiansX1)*panRate1 
               tempX = self.clamp(tempX, self.panLimitsX.getX(), self.panLimitsX.getY()) 
               self.target.setX(tempX) 
               tempY = self.target.getY()-math.cos(angleradiansX1)*panRate1 
               tempY = self.clamp(tempY, self.panLimitsY.getX(), self.panLimitsY.getY()) 
               self.target.setY(tempY) 
               self.turnCameraAroundPoint(0,0) 
            if moveX: 
               tempX = self.target.getX()-math.sin(angleradiansX2)*panRate2 
               tempX = self.clamp(tempX, self.panLimitsX.getX(), self.panLimitsX.getY()) 
               self.target.setX(tempX) 
               tempY = self.target.getY()+math.cos(angleradiansX2)*panRate2 
               tempY = self.clamp(tempY, self.panLimitsY.getX(), self.panLimitsY.getY()) 
               self.target.setY(tempY) 
               self.turnCameraAroundPoint(0,0) 
            # These two blocks finalize the math necessary to find the new camera position and apply the transformation to the 
            # camera's TARGET. Then turnCameraAroundPoint is called with 0s for rotation, and it resets the camera position based 
            # on the position of the target. The x and y values are clamped to the pan limits before they are applied. 
         print(self.target) 
         self.mx=mpos.getX() 
         self.my=mpos.getY() 
         # The old mouse positions are updated to the current mouse position as the final step. 
          
      return task.cont
    
    #selection starts here
    def Is3dpointIn2dRegion(self, node, point1,point2,point3d): 
      #"""This function takes a 2d selection box from the screen as defined by two corners 
      #and queries whether a given 3d point lies in that selection box 
      #Returns True if it is 
      #Returns False if it is not""" 
      #node is the parent node- probably render or similar node 
      #point1 is the first 2d coordinate in the selection box 
      #point2 is the opposite corner 2d coordinate in the selection box 
      #point3d is the point in 3d space to test if that point lies in the 2d selection box 
       
      # Convert the point to the 3-d space of the camera 
      p3 = base.cam.getRelativePoint(node, point3d) 

      # Convert it through the lens to render2d coordinates 
      p2 = Point2() 
      if not base.camLens.project(p3, p2): 
         return False 
       
      r2d = Point3(p2[0], 0, p2[1]) 

      # And then convert it to aspect2d coordinates 
      a2d = aspect2d.getRelativePoint(render2d, r2d) 
       
      #Find out the biggest/smallest X and Y of the 2- 2d points provided. 
      if point1.getX() > point2.getX(): 
         bigX = point1.getX() 
         smallX = point2.getX() 
      else: 
         bigX = point2.getX() 
         smallX = point1.getX() 
          
      if point1.getY() > point2.getY(): 
         bigY = point1.getY() 
         smallY = point2.getY() 
      else: 
         bigY = point2.getY() 
         smallY = point2.getY() 
       
      pX = a2d.getX() 
      pY = a2d.getZ()  #aspect2d is based on a point3 not a point2 like render2d. 
       
      if pX < bigX and pX > smallX: 
         if pY < bigY and pY > smallY: 
             
            return True 
         else: return False 
      else: return False
    
app = WarGames3D()
app.run()
        

I ran this code, it works fine, other than the selection part… any help would be appreciated.

Are you sure the selection rectangle covers the dozer’s origin ?
Try parenting the default axis model to it, to see where the origin is.