"lock" camera to arbitrary vector

I have built an editor-style app using Panda3d, and my users would like the ability to lock the camera to an arbitrary vector. For simplicity, imagine you have the forward, up & left vectors of a model. The camera should be able to be locked so it looks down one of these vectors. It should be able to move along (backwards and forwards) the vector, and rotate around it, but not rotate away from it (there’s probably a more correct way to explain that, sorry).

How would you approach this using the panda3d camera?

setPos and setHpr accept an optional first argument, which is the node to make the transformation relative to. So, if you wanted to move the camera along its local x axis, you would use

base.camera.setPos(base.camera, 1, 0, 0)

Hmm, it would take too long to explain why this is the case, but the vectors that the camera should be locked to are not related to any panda node objects… So there is just some arbitrary vector in worldspace, like [-0.96223659 -0.12102822 0.2438297 ]. How would I lock the camera so that it always looks in that direction in worldspace?

to be clear, it would always look in that direction, but the user should still be able to move the camera around and rotate the view of the camera, but only along the axis of that vector.

The easiest way I can think of is to create a dummy node, place it at that desired arbitrary vector, and then call base.camera.lookAt(dummy) to cause it to look at the node.

If I’m reading your post correctly, to easily allow for the rotation around that node, you could reparent the camera to it and rotate the dummy node itself, which would cause the camera to swing around while still looking at it.

I don’t want it to look AT that point in space, i want the camera to always look in the direction represented by that vector.

The approach that comes to mind for me is to first orient the camera along the desired vector–there are a few ways of doing this, including the “lookAt” method mentioned above–and to then in the editor’s internal logic only move the camera relative to itself. In particular, rotations about the camera’s y-axis should result in rotations around the desired vector, I believe.

So, currently, I’ve gotten away with just using the default camera. It sounds like I need to implement my own custom camera. Any tips/tutorials here would be greatly appreciated.

In case it helps to explain what I mean, here’s a screenshot of my landmark editor. The yellow lines represent the estimated axes of the auricle (i.e. outer ear). In order to more accurately/precisely place certain landmarks, the users would like to lock the camera so it always looks down one of these axes.

Your question is clear, but it is not clear what causes difficulties. You set the camera as you want, and disable updating the desired positions.

from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        base.disableMouse()
        
        self.sensitivity = 0.05
        
        room1 = loader.loadModel('environment')
        room1.reparentTo(render)

        self.keyMap = {"Mouse3":0}

        base.accept("mouse3", self.set_key, ["Mouse3",1])
        base.accept("mouse3-up", self.set_key, ["Mouse3",0])

        taskMgr.add(self.cam_control, 'cam_control')
        
    def set_key(self, key, value):
        self.keyMap[key] = value

    def cam_control(self, task):
        if (self.keyMap["Mouse3"] != 0):

            md = base.win.getPointer(0)

            if base.win.movePointer(0, base.win.getXSize()//2, base.win.getYSize()//2):
                camera.setH(camera.getH() - (md.getX() - base.win.getXSize()/2)*self.sensitivity)
                #camera.setP(camera.getP() - (md.getY() - base.win.getYSize()/2)*self.sensitivity)

        return task.cont

app = MyApp()
app.run()

In this example, there is no vertical movement, I commented out this line, but you can make a condition. That is, as you have already been told, you need to bind to a node where you will only calculate the position, and ignore the rotation. I would just update the position with the node using the setPos() method

Indeed, it sounds like you might be well-served to implement your own camera controls in this case.

The code provided by serega above looks like a good starting point at the least, so I’ll second that as a guide to how to go about your implementation!

(Note in particular the call to “base.disableMouse()”–that disables the default camera control, allowing you to control the camera yourself, as I recall.

I will add that in said code you could replace “base” with “self” in the call–that is, giving "self.disableMouse()–as the class in which this takes place is the current ShowBase instance.)

So, maybe a dumb question: Is there any code out there that emulates the current default camera? I know my users would be unhappy if the camera feel/functionality changed just to support this one feature, so I need the rest of the camera to behave as close to how it currently does as possible.

This is not possible, but you can copy some of the camera code from showbase to combine the logic with your own. By default, the camera is a quick link to the overview, it was not intended as a universal one.

Ah, hmm, I don’t know. And that is a fair point, I feel!

You could search the forum. I doubt that it’s been done, as I doubt that many have had a reason to do it, but you never know.

That aside, you could, as suggested above, try to copy the logic from the engine’s source code. This may involve converting some C++ logic to Python, however.

Otherwise, you could perhaps try to figure out the behaviour of the default camera controls and implement them yourself.

Sure, this is for my day-job, though, and the client is probably not willing to pay for the time required for this particular feature. Thanks for your help/advice, however!

Hmm… One more thought: I don’t know how feasible this is, but you could perhaps find the camera-control code in the engine source, modify that to include the feature that you want, and then build the engine locally. That way you wouldn’t have to re-implement everything.

It might complicate updates to the engine, however. (And may require C++ instead of Python, if I’m right about where the relevant code resides.)

Yeah, it’s worth a look. Looking at the source now (first time I’ve looked at the panda3d c++ source)…if you know off-hand, where is the camera code implemented? I noticed this in the docs: https://docs.panda3d.org/1.10/python/reference/panda3d.core.Trackball?highlight=trackball#panda3d.core.Trackball

I assume that is the code that drives the default camera when it’s in ‘trackball’ mode…

looks like this is it: https://github.com/panda3d/panda3d/blob/master/panda/src/tform/trackball.h

I don’t know with confidence–this isn’t something that I’ve worked with myself, I don’t think–but in looking around last night I likewise came upon the “trackball” class. (I looked at the “cxx” file, specifically.) That would indeed be my guess.

It probably isn’t necessary to go to such great lengths. The code below is my attempt to combine the built-in trackball behavior with the custom functionality you described:

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


class NavigationManager:

    def __init__(self, showbase, cam_vec):

        self.showbase = showbase
        self.task_mgr = showbase.task_mgr
        self.mouse_watcher = showbase.mouseWatcherNode
        self.cam = showbase.camera
        self.cam_vec = cam_vec
        self.mouse_prev = Point2()
        self.roll_speed = 100.
        self.dolly_speed = 10.
        self.listener = listener = DirectObject()
        listener.accept_once("l", self.lock_cam)

    def lock_cam(self):

        self.showbase.disable_mouse()
        mat = Mat4()
        rotate_to(mat, Vec3.forward(), self.cam_vec.normalized())
        tmp_node = NodePath("tmp")
        tmp_node.set_mat(mat)
        self.cam.set_pos(self.showbase.cam.get_pos(self.showbase.render))
        self.cam.set_hpr(tmp_node.get_hpr())
        self.showbase.cam.set_mat(Mat4.ident_mat())
        self.listener.accept_once("mouse1", self.start_roll)
        self.listener.accept_once("mouse3", self.start_dolly)
        self.listener.accept_once("u", self.unlock_cam)

    def unlock_cam(self):

        xform = self.cam.get_transform().get_inverse()
        pos = xform.get_pos()
        hpr = xform.get_hpr()
        self.showbase.mouseInterfaceNode.set_pos(pos)
        self.showbase.mouseInterfaceNode.set_hpr(hpr)
        self.showbase.enable_mouse()
        self.task_mgr.remove("transform_cam")
        self.listener.ignore("mouse1")
        self.listener.ignore("mouse3")
        self.listener.ignore("mouse1-up")
        self.listener.ignore("mouse3-up")
        self.listener.accept_once("l", self.lock_cam)

    def start_roll(self):

        self.listener.ignore("mouse3")
        self.listener.accept_once("mouse1-up", self.stop_roll)
        self.mouse_prev = Point2(self.mouse_watcher.get_mouse())
        self.task_mgr.add(self.roll_cam, "transform_cam")

    def stop_roll(self):

        self.task_mgr.remove("transform_cam")
        self.listener.accept_once("mouse1", self.start_roll)
        self.listener.accept_once("mouse3", self.start_dolly)

    def roll_cam(self, task):

        if self.mouse_watcher.has_mouse():
            mouse_pos = self.mouse_watcher.get_mouse()
            d = (mouse_pos - self.mouse_prev)[1]
            self.cam.set_r(self.cam, d * self.roll_speed)
            self.mouse_prev = Point2(mouse_pos)

        return task.cont

    def start_dolly(self):

        self.listener.ignore("mouse1")
        self.listener.accept_once("mouse3-up", self.stop_dolly)
        self.mouse_prev = Point2(self.mouse_watcher.get_mouse())
        self.task_mgr.add(self.dolly_cam, "transform_cam")

    def stop_dolly(self):

        self.task_mgr.remove("transform_cam")
        self.listener.accept_once("mouse3", self.start_dolly)
        self.listener.accept_once("mouse1", self.start_roll)

    def dolly_cam(self, task):

        if self.mouse_watcher.has_mouse():
            mouse_pos = self.mouse_watcher.get_mouse()
            d = (mouse_pos - self.mouse_prev)[1]
            self.cam.set_y(self.cam, d * self.dolly_speed)
            self.mouse_prev = Point2(mouse_pos)

        return task.cont


showbase = ShowBase()
smiley = showbase.loader.load_model("smiley")
smiley.reparent_to(showbase.render)
dir_vec = Vec3(-1., 1., -1.)
nav_mgr = NavigationManager(showbase, dir_vec)
showbase.run()

To lock the camera to the desired direction vector (in the code sample, this is a hard-coded dir_vec vector), press L on the keyboard. Then you can test the rolling and dollying of the camera (which I think is what you wanted, judging from your description) by left- (resp. right-) dragging the mouse vertically. To unlock the camera again, press U. Using the smiley model as a guide, you should see that the camera remains in the same position and orientation when doing so. The built-in trackball controls will once again be active.

Hopefully this will save you some time and work.

Note that the smiley will suddenly jump to a different position on screen when locking the camera. This is normal, as the orientation of the camera is changed, but its position isn’t. Perhaps it would be worthwhile to lerp the camera hpr using a LerpHprInterval so your users don’t feel too disoriented when this happens.

EDIT:
Forgot to mention that setting the orientation of the camera to look in the direction of the specified vector is accomplished using a call to rotate_to, which I would like to personally nominate as the most underrated/underused math function in Panda :stuck_out_tongue: .

2 Likes