Placing 3D Objects at Edge of Window

Hey Everyone,

I’d like to know if it’s possible to have Panda automatically place some BulletPlaneShapes on the edge of the screen independent of monitor resolution. The gist is to use them as walls to contain a ball so that it will bounce off the edge of the screen, but the important thing is that it needs to work for all monitors and resolutions. This will be using an orthographic camera lens with a film size of (screenWidth * 0.01, screenHeight * 0.01) if that makes a difference. The ball will only be moving up,down, left, and right. No forward or back as it will be tightly placed between two more planes that prevent it from going in those directions. I’m going for a 2.5D look.

Is that possible?

Thank you for any advice that you can offer.

I don’t think that this is something that Panda will do for you; I think that it calls for scripting.

That said, it should be fairly straightforward: You have access to the size of the window, and can use that data to determine where to position the plane-shapes. Since you’re using an orthographic lens, the relationship between the two should be linear, I think.

(You can also do this by using the “extrude” method to convert a 2D window-position to a 3D world position, if I’m not much mistaken. However, since you’re using an orthographic lens, even that might be a little more complicated than just applying the window-size!)

If the player can resize the window, then you might also want to catch window-events in order to reposition your planes.

Thank you for the quick reply. Would you be able to give me an example of how to convert a 2D window-position to a 3D world position, or point me to an example? I’m looking through the manual now and I don’t quite understand how I could use it to get the 3D position.

The following seems like an easy way to align objects to the sides of the window, although I’m not sure if it will work with your particular setup:

from direct.showbase.ShowBase import ShowBase
from panda3d.core import *


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        # set up a light source
        p_light = PointLight("point_light")
        p_light.set_color((1., 1., 1., 1.))
        self.light = self.camera.attach_new_node(p_light)
        self.light.set_pos(5., -100., 7.)
        self.render.set_light(self.light)

        # create an orthographic lens
        lens = OrthographicLens()
        self.camNode.set_lens(lens)
#        self.ortho_lens = lens
        self.camLens = lens
        # if the new lens is assigned to `self.camLens`, the lens event
        # to be associated with this lens might be called recursively,
        # causing the application to hang;
        # this can be prevented using a flag that tells the application
        # whether the event should be handled (when ShowBase attempts to do
        # this automatically) or not (when manually updating the film size)
        self.handle_lens_event = True
        self.camLens.change_event = "lens-event"
        self.accept("lens-event", self.__handle_lens_event)

        # use the smiley model to represent objects to be kept at the sides
        # of the window
        self.smiley1 = self.loader.load_model("smiley")
        self.smiley1.reparent_to(self.cam)
        self.smiley2 = self.smiley1.copy_to(self.cam)
        self.smiley3 = self.smiley1.copy_to(self.cam)
        self.smiley4 = self.smiley1.copy_to(self.cam)

    def __handle_lens_event(self, lens):

        if not self.handle_lens_event:
            self.handle_lens_event = True
            # do not handle manually setting the film size of `self.camLens`
            return

        aspect_ratio = self.get_aspect_ratio(self.win)
        size = 50.  # change this to the desired horizontal film size
        # make sure this method doesn't get called recursively due to updating
        # the film size of `self.camLens` after the following line of code
        self.handle_lens_event = False
        self.camLens.film_size = (size, size / aspect_ratio)
#        self.ortho_lens.film_size = (size, size / aspect_ratio)
        self.smiley1.set_pos(-size * .5, 10., 0.)
        self.smiley2.set_pos(size * .5, 10., 0.)
        self.smiley3.set_pos(0., 10., -size * .5 / aspect_ratio)
        self.smiley4.set_pos(0., 10., size * .5 / aspect_ratio)


app = MyApp()
app.run()

Whenever the window size changes, the lens event for the built-in ShowBase.camLens gets fired, causing the __handle_lens_event handler method to be called, which in turn updates the film size of the new orthographic lens, as well as the positions of the objects to be aligned to the window sides, according to that new film size.

Not sure if this is what you’re looking for, but maybe it can help.

1 Like

That looks like pretty much what I had in mind! (Although I was thinking of using the window-event rather than the lens-event.)

The other approach is to use the “extrude” method, which more-directly converts a 2D position to a 3D position.

(Note again that this approach is a little less straightforward than the above; I include it for completeness, and in case it’s what you were asking for in your second post, I think.)

First of all, here for reference and examination is the API entry:

Now, in short the method in question is this:
To start with, note that a 2D point on the screen doesn’t correspond to a single 3D point in the world–after all, that 2D point corresponds essentially to looking in a given direction, and many points in the world may be located within the line of that direction. Instead, a 2D point corresponds to a vector, starting at the location of the camera and extending out to infinity.

So, “extrude” takes a 2D point, and converts it to a vector. There are a few forms of this that the engine offers–converting to a Vec3, or converting to points on the near- and far- planes, etc. There’s even a form that takes an extra “depth” component and thus produces a single 3D point, being the one that’s located on the vector at that “depth”.

The inputs for this method correspond to the dimensions used by the “render2d” scene-graph node: the left-most side of the screen has an x-coordinate of -1, the right most side an x-coordinate of 1, the bottom-most side a y-coordinate of -1, and the top-most side a y-coordinate of 1. That is, it has the range (-1, 1) in both dimensions. And this remains regardless of the size or aspect ratio of the screen.

In your case this is useful, as it means that you can just pass the same values to “extrude” whenever the window is changed, and get updated values out of it.

So, what you might do when the window is changed is something like this:
(I’m assuming that your “depth” (i.e. “into the screen”) axis is the z-axis. If this isn’t the case, adjust the last step below accordingly.)

# These will contain the results of the "extrude" method
nearPt = Point3()
farPt = Point3()
# Here use whatever lens you are employing,
# whether that's your own or base.camLens.
#
# The point (-1, 0) corresponds to the middle of the left-hand
# side of the screen, I believe.
self.lens.extrude(Vec2(-1, 0), nearPt, farPt)

# With a perspective lens this next step gets a little more
# complicated, but thankfully with an orthographic lens we can
# just grab the x- and y- coordinates and set the z- coordinate as
# we please! ^_^
self.leftWall.setPos(nearPt.x, nearPt.y, self.desiredZCoord)

Then repeat for the right, bottom, and top.

2 Likes

Thank you both for helping me. It’s very much appreciated. So I’ve got sort of a hybrid setup now using a mix of the two suggestions. For testing purposes I’ve got my left wall using the extrude method and the floor and right wall using the other method. They seem to be getting placed in the same spot with either method, however I’m noticing that the smaller I set my film size the more the walls move inward toward the center of the screen. I’m not sure how to keep the walls perfectly at the edge as I want the film size to be around 20.

I’m thinking it may be due to my setting up my orthographic lens slightly different than what is shown in Epihaius’ example because when I tried to copy it exactly I could not longer see my bullet objects anywhere. I’m very new to Panda3D and coding so it’s likely I’m doing something wrong with where I’m rendering things but I’m not sure.

In Epihaius’ example the smileys seem to always land exactly on the edge of the screen regardless of the film size, which is not how my adaptation is working.

Here’s is my current code.

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from panda3d.core import Vec4, Vec3, Vec2, Point3, LVector3, LColor, Material, BitMask32, OrthographicLens, AmbientLight, Spotlight, WindowProperties, Lens, CollisionRay
from panda3d.bullet import BulletWorld, BulletPlaneShape, BulletSphereShape, BulletRigidBodyNode, BulletDebugNode
import sys, random

staticObjects = []
marbleActors = []
marbleCount = 0
maxObstacleCount = 3
maxMarbleCount = 500

class MyApp(ShowBase):
    
    def __init__(self):
      
        ShowBase.__init__(self)
        
        # Escape quits.
        self.accept("escape", sys.exit)
      
        screenWidth = int(self.pipe.getDisplayWidth())
        screenHeight = int(self.pipe.getDisplayHeight())
        windowProperties = WindowProperties()
        windowProperties.setSize(screenWidth,screenHeight)
        windowProperties.setForeground(1)
        windowProperties.set_undecorated(True)
        windowProperties.setTitle("Marbles")
        windowProperties.set_icon_filename("Icons\Window_Icon.ico")
        self.win.requestProperties(windowProperties)
        self.setBackgroundColor((0, 0, 0, 1))

        #"""
        # Disables the camera controls.
        self.disableMouse()
        self.lens = OrthographicLens()
        self.lens.setNearFar(-10, 10)
        self.camLens.change_event = "lens-event"
        self.accept("lens-event", self.__handle_lens_event)
        #"""

        debugNode = BulletDebugNode('Debug')
        debugNode.showWireframe(True)
        debugNode.showConstraints(True)
        debugNode.showBoundingBoxes(False)
        debugNode.showNormals(True)
        debugNP = self.cam.attachNewNode(debugNode)
        debugNP.show()

        # World
        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -9.81))
        self.world.setDebugNode(debugNP.node())

         # Floor
        floorShape = BulletPlaneShape(Vec3(0, 0, 1), 1)
        floorNode = BulletRigidBodyNode('floor')
        floorNode.addShape(floorShape)
        floorNode.setRestitution(0.6)
        floorNode.setFriction(100)
        self.floorNP = self.cam.attachNewNode(floorNode)
        self.floorNP.reparent_to(self.cam)
        self.world.attachRigidBody(floorNode)
        
        # Left Wall
        leftWallShape = BulletPlaneShape(Vec3(1, 0, 0), 1)
        leftWallNode = BulletRigidBodyNode('Left Wall')
        leftWallNode.addShape(leftWallShape)
        leftWallNode.setRestitution(1)
        leftWallNode.setFriction(10)
        self.world.attachRigidBody(leftWallNode)
        self.leftWallNP = self.cam.attachNewNode(leftWallNode)
        self.leftWallNP.reparent_to(self.cam)
        
        # Right Wall
        rightWallShape = BulletPlaneShape(Vec3(-1, 0, 0), 1)
        rightWallNode = BulletRigidBodyNode('Right Wall')
        rightWallNode.addShape(rightWallShape)
        rightWallNode.setRestitution(1)
        rightWallNode.setFriction(10)
        self.world.attachRigidBody(rightWallNode)
        self.rightWallNP = self.cam.attachNewNode(rightWallNode)
        self.rightWallNP.reparent_to(self.cam)
        
        # Load all models.
        # Marble Model
        self.marble = loader.loadModel('Models/Marble')
        
        #self.marble.setColor(LColor(1, 1, 1, 1) ** 5)  

        # Load all textures.
        # Marble Textures
        marbleTextures = ["Models/Textures/Marble_1.png", "Models/Textures/Marble_2.png", "Models/Textures/Marble_3.png", "Models/Textures/Marble_4.png"]
        self.marbleTextureObjects = []
        for texture in marbleTextures:
            self.marbleTextureObjects.append(self.loader.loadTexture(texture))

        self.taskMgr.add(self.createMarble, "Create Marble")
        self.taskMgr.add(self.updatePhysics, 'Update Physics')
        

    def __handle_lens_event(self, lens):

        self.aspect_ratio = self.get_aspect_ratio(self.win)
        self.filmSize = 20
        self.lens.film_size = (self.filmSize, self.filmSize / self.aspect_ratio)
        self.cam.node().setLens(self.lens)

        # These will contain the results of the "extrude" method
        nearPt = Point3()
        farPt = Point3()
        # Here use whatever lens you are employing,
        # whether that's your own or base.camLens.
        #
        # The point (-1, 0) corresponds to the middle of the left-hand
        # side of the screen, I believe.
        self.lens.extrude(Vec2(-1, 0), nearPt, farPt)

        # With a perspective lens this next step gets a little more
        # complicated, but thankfully with an orthographic lens we can
        # just grab the x- and y- coordinates and set the z- coordinate as
        # we please! ^_^
        self.leftWallNP.setPos(nearPt.x, nearPt.y, 0)

        self.floorNP.setPos(0, 0, -self.filmSize * 0.5 / self.aspect_ratio)
        #self.leftWallNP.setPos(-self.filmSize * 0.5, 0, 0)
        self.rightWallNP.setPos(self.filmSize * 0.5, 0, 0)

        # Create all static objects and obstacles.
        for x in range(maxObstacleCount):
            # Generic Sphere Obstacle
            # Hpr affects rotation.
            self.marble.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
            randomScale = round(random.uniform(0.5, 0.5), 2)
            self.marble.setScale(randomScale, randomScale, randomScale)
            self.marble.setTexture(random.choice(self.marbleTextureObjects))
            material = Material()
            # Adjusts the colors of the reflection off the marble. (R, G, B, A)
            material.setSpecular((1, 1, 1, 1))
            material.setShininess(90)
            #material.setEmission((0, 0, 0, 0)) #Make this material max brightness regardless of lighting.
            self.marble.setMaterial(material, 1)
            marbleCollisionShape = BulletSphereShape(randomScale)
            marbleNode = BulletRigidBodyNode('Marble')
            marbleNode.setMass(0)
            marbleNode.setRestitution(0.5)
            marbleNode.setFriction(0)
            marbleNode.addShape(marbleCollisionShape)
            marbleNodePath = self.cam.attachNewNode(marbleNode)
            marbleNodePath.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((-self.filmSize * 0.5 / self.aspect_ratio), (self.filmSize * 0.5 / self.aspect_ratio)))
            # Make sure there are no collisions happening with the obstacles.
            while (self.world.contactTest(marbleNode)).getNumContacts() > 0:
                marbleNodePath.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((-self.filmSize * 0.5 / self.aspect_ratio), (self.filmSize * 0.5 / self.aspect_ratio)))
            self.world.attachRigidBody(marbleNode)
            self.marble.copyTo(marbleNodePath)
            staticObjects.append(marbleNodePath)
        
        # Attach a point light to the camera so that the marbles are always lit.
        spotLight = Spotlight("Spot Light")
        spotLight.setLens(PerspectiveLens())
        spotLightNodePath = self.cam.attachNewNode(spotLight)
        spotLightNodePath.setPos(random.uniform(-40, 40), -30, 40)
        spotLightNodePath.lookAt(self.floorNP)
        self.cam.setLight(spotLightNodePath)
        
        # Add ambient lighting to the scene.
        ambientLight = AmbientLight("Ambient Light")
        ambientLight.setColor(Vec4(0.2, 0.2, 0.2, 1))
        ambientLightNodePath = self.cam.attachNewNode(ambientLight)
        self.cam.setLight(ambientLightNodePath)
        
        self.cam.setShaderAuto()
        

    # This task runs every 2 seconds.
    def createMarble(self, task):
        global marbleCount
        
        if task.time < 2.0:
            return Task.cont

        if marbleCount < maxMarbleCount:
            marbleCount = marbleCount + 1
            
            # Set rotation. (X, Y, Z)
            self.marble.setHpr(random.randint(0, 360), random.randint(0, 360), random.randint(0, 360))
            #marble.flattenLight()
            # Randomize the scale of the model.
            randomScale = round(random.uniform(0.2, 0.2), 2)
            self.marble.setScale(randomScale, randomScale, randomScale)
            # Set a random texture for the marble.
            self.marble.setTexture(random.choice(self.marbleTextureObjects))
            # This section deals with adding a specular highlight to the ball to make it look shiny.  Normally, this is specified in the .egg file.
            material = Material()
            # Adjusts the collors of the reflection off the marble. (R, G, B, A)
            material.setSpecular((1, 1, 1, 1))
            material.setShininess(90)
            self.marble.setMaterial(material, 1)
            marbleCollisionShape = BulletSphereShape(randomScale)
            marbleNode = BulletRigidBodyNode('Marble')
            marbleNode.linear_factor = LVector3(1, 0, 1)
            marbleNode.setMass(3.0)
            marbleNode.setRestitution(0.55)
            marbleNode.setFriction(0.5)
            marbleNode.addShape(marbleCollisionShape)
            marbleNodePath = self.cam.attachNewNode(marbleNode)
            marbleNodePath.setPos(random.uniform((-self.filmSize * 0.5), (self.filmSize * 0.5)), 0, random.uniform((self.filmSize * 0.5 / self.aspect_ratio + 5), (self.filmSize * 0.5 / self.aspect_ratio + 10)))
            marbleNodePath.setColor(1,1,1,1)

            self.world.attachRigidBody(marbleNode)
            self.marble.copyTo(marbleNodePath)

            marbleActors.append(marbleNode)
                
        return Task.again

      # Update Physics
    def updatePhysics(self, task):
        dt = globalClock.getDt()
        self.world.doPhysics(dt)
        return Task.cont
      
      
app = MyApp()
app.run()

This is simply because you’re effectively “zooming in” when decreasing the film size, so everything becomes bigger, including the offsets of the planes from the window edges.

Anyway, this offset seems caused by the second parameter passed into the constructor of the BulletPlaneShape; when I set that to zero, it seems to work well. Since I don’t work with Bullet myself, I’m not sure if setting that value to zero doesn’t cause any other problems, though.
If you need to keep it to 1, then you can account for it by placing e.g. the left wall 1 additional unit to the left, the right wall one extra unit to the right and the floor one more unit down.

Setting the second constructor to 0 seems to have fixed the issue for me as well. I’m not really sure what it is used for as I pulled them from an example and it did not go over it. Things seem to be working well with it set to 0 still so I’m going to leave it at that. Thanks for the help!