Model vibrates when camera pans

I’m trying to implement a way to move the camera view around in my program and I’ve used the functionality provided in the Roaming Ralph sample program.

It’s pretty much exactly what I need for now, however, it doesn’t quite work the same in my program.

The camera view can move around fine, but the model to which the camera is attached vibrates weirdly on the spot and I can’t seem to figure out why.

Here is my code:

from direct.showbase.ShowBase import ShowBase
from direct.interval.IntervalGlobal import *
from panda3d.core import WindowProperties
from panda3d.core import Vec3, Vec4, VBase4
from panda3d.core import GeoMipTerrain
from panda3d.core import AmbientLight, PointLight, DirectionalLight, Spotlight, PerspectiveLens
from panda3d.core import NodePath, PandaNode, Camera, TextNode
import sys
import os
print("Imports successful!")

# Measuring units in Panda3D are arbitrary, so I've set 1 unit to equal 1m
MARS_RADIUS = 3389500 # m
LANDER_RADIUS = 1 # m
MARS_DAY = 88620 # s

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

        # The default mouse controls take precedence over base.camera (or self.camera in this case, since MarsLander inherits from ShowBase)
        # Disabling default mouse controls allows base.camera to be manipulated
        self.disableMouse()

        self.load_mars()
        self.load_lander()
        
        # Create a floater object, which floats 2 units above the lander
        # We use this as a target for the camera to look at
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(self.lander)
        self.floater.setZ(0.0)

        # Create a keymap to store which keys are currently pressed
        self.keyMap = {
            "cam_left": 0,
            "cam_right": 0,
            "cam_up": 0,
            "cam_down": 0
        }
        
        # Accept the control keys for the program
        self.accept("escape", sys.exit)
        self.accept("a", self.setKey, ["cam_left", True])
        self.accept("a-up", self.setKey, ["cam_left", False])
        self.accept("d", self.setKey, ["cam_right", True])
        self.accept("d-up", self.setKey, ["cam_right", False])
        self.accept("w", self.setKey, ["cam_up", True])
        self.accept("w-up", self.setKey, ["cam_up", False])
        self.accept("s", self.setKey, ["cam_down", True])
        self.accept("s-up", self.setKey, ["cam_down", False])
        
        taskMgr.add(self.move_camera, "move_camera_task")
        
        self.configure_camera()
        self.lander_light()
    
    def move_camera(self, task):
        dt = globalClock.getDt()
        
        if (self.keyMap["cam_left"]):
            self.camera.setX(self.camera, +100 * dt)
        if (self.keyMap["cam_right"]):
            self.camera.setX(self.camera, -100 * dt)
        if (self.keyMap["cam_up"]):
            self.camera.setZ(self.camera, -100 * dt)
        if (self.keyMap["cam_down"]):
            self.camera.setZ(self.camera, +100 * dt)
        
        self.camera.lookAt(self.floater)

        #print(self.camera.getPos())
        return task.cont
    
    def setKey(self, key, value):
        '''Records the state of the arrow keys'''
        self.keyMap[key] = value

    def load_mars(self):
        '''Load Mars model into the program, attach it to the scene graph (root node in this case) so it can render, set its position and its scale'''
        self.mars = self.loader.loadModel("models_and_textures/mars/mars.obj")
        self.mars.reparentTo(self.render)
        self.mars.setPos(0, 0, 0)
        self.mars_model_size = float(str(self.mars.getBounds()).split()[-1])
        self.mars_scale_factor = MARS_RADIUS / self.mars_model_size
        self.mars.setScale(self.mars_scale_factor)
    
    def load_lander(self):
        '''Load lander model into the program, render it, set position and scale'''
        self.lander = self.loader.loadModel("models_and_textures/lander/lander.obj")
        self.lander.reparentTo(self.render)

        self.lander_model_size = float(str(self.lander.getBounds()).split()[-1])
        self.lander_scale_factor = LANDER_RADIUS / self.lander_model_size
        self.lander.setScale(self.lander_scale_factor)
        self.lander.setPos(0, -1.2 * MARS_RADIUS, 0)

        #print(self.lander.getBounds())
        print("Lander position relative to origin is: ", tuple(self.lander.getPos()))
    
    def configure_camera(self):
        '''Set up the camera to always be 'connected' to and looking at the lander'''
        self.camera.reparentTo(self.lander)
        self.camera.setPos(0, -100, 0)
        #self.camera.lookAt(0, 0, 0)
        print("Camera position relative to lander is: ", tuple(self.camera.getPos()), "\n")
        #self.cameraNode = self.render.attachNewNode('cameraNode')
        #self.camera.wrtReparentTo(self.cameraNode)
        # The default field of view angle is 40 degrees, increasing it to 50 widens the view field
        self.camLens.setFov(50)
        # The default far distance of the lens frustum is set at 1000.0 units, so Mars is too far away to be rendered
        # Increasing it to 10 million puts Mars in the lens frustum so it can be rendered
        self.camLens.setFar(10000000)
    
    def lander_light(self):
        '''Materials on models don't show up without a light specifically aimed at the model'''
        frontLight = PointLight("front light")
        frontLight.setColor(VBase4(1, 1, 1, 1))
        self.frontLightNodePath = self.render.attachNewNode(frontLight)
        self.frontLightNodePath.setPos(self.lander.getPos() + (10, 10, 2))
        self.lander.setLight(self.frontLightNodePath)
        
        backLight = PointLight("back light")
        backLight.setColor(VBase4(1, 1, 1, 1))
        self.backLightNodePath = self.render.attachNewNode(backLight)
        self.backLightNodePath.setPos(self.lander.getPos() + (10, -10, 2))
        self.lander.setLight(self.backLightNodePath)


mars_lander = MarsLander()
mars_lander.run()

This video shows the issue:

That jittering is due to the limited precision of the float numbers (Roughly a float contains at most 6 significant digits). There are lots of workaround :

  • first scale down your scene (e.g. use one Km as unit or even more)
  • set the camera at the origin and displace your nodes when you move your camera
  • shift the origin of your nodes towards the camera
  • recompile Panda to use doubles instead of float.
  • Apply non linear scaling

In Cosmonium, using several of the techniques above, I can orbit a satellite (like the ISS) and have the full scale Earth in the background without any jittering or loss of precision.

1 Like

I had a feeling that might be the cause.

I’ve tried tips 1 and 4 together, but they haven’t worked yet.
I think I know how to implement tip 2, but I don’t understand tips 3 and 5.
Could you elaborate on those please?

Also, I had another idea for moving the camera around. Rather than translating the camera and getting it to look at the lander at every frame, would it be better to set the focal point at the origin of the lander, then simply use setHpr() for the camera (since the pivot would now be at the position of the lander)?

The tip 3, shift the origin of your nodes towards the camera, is useful when your object is actually composed of several parts. For example, a planet is usually divided in chunks, parented to the root node of the planet. The idea is to move the root node towards the camera, usually at the surface of the planet, and apply the opposite transformation on the chunks. So when the global transformation of each chunk is calculated the two transformation cancel and you don’t have loss of precision on the chunks near the camera.
Note that if you are using Panda compiled with double as default type, this should not be needed.

The tip 5, apply non linear scaling, is a perspective trick, for far object you scale them down using a distance related function, usually an inverse log or 1/x. The idea is to place the object much more closer to the camera than it actually is and scale it down accordingly to keep the apparent size identical. For example, if you use a log10 scaling, instead of having a planet with a 10000km diameter at 100000km, you have a planet with diameter 10000 * 5/100000 at 5 units. The function must be a bit more complicated to avoid infinities or too small size, but that’s the idea.

1 Like

Ah I see now.

The Mars model is actually a free model I downloaded, so I’m not sure if it has several parts or how it was made. But I think I get how this method works.

As for scaling, it turns out, the issue was the relative scale between Mars and the lander. Mars was originally of the order of a million times bigger than the lander (to reflect real life), but if I reduce that scale factor to 1000 (or even less!), then the jittering goes away and the simulation is smooth.

I guess it doesn’t matter that the lander is now 1 km across, since initially it’s far away from Mars.

However, I do still get this issue:

Is that the same issue as before or is it something else?
Strangely, it only happens when moving the camera up and down, but not when moving it left and right.

I think the problem is somehow related to lookAt() or the way you are orienting your camera, you orientation suddenly wraps around from -180° to 180° wrt the horizontal axis and your camera flips over.

Try adding an up direction when using lookAt() to avoid that.

1 Like

How do I add an up direction to lookAt()?

I think something like this should work :

camera.lookAt(target, up=LVector3.up())
1 Like

I tried that, but got this error:

TypeError: Arguments must match:
look_at(const NodePath self, const NodePath other)
look_at(const NodePath self, const LPoint3f point)
look_at(const NodePath self, const NodePath other, const LPoint3f point)
look_at(const NodePath self, const LPoint3f point, const LVector3f up)
look_at(const NodePath self, const NodePath other, const LPoint3f point, const LVector3f up)
look_at(const NodePath self, float x, float y, float z)
look_at(const NodePath self, const NodePath other, float x, float y, float z)

So I looked at the API reference at the lookAt method. A while later I managed to figure out it was saying, so I tried a lot of different arguments, but nothing worked.

Rotations are not my area of expertise, I usually tinker until I get the expected result :slight_smile: So I guess a Panda expert should chime in.

FWIW, in Cosmonium I’m calculating the orientation manually and use the function lookAt() to create the rotation to apply to the camera :

    orientation = LQuaternion()
    lookAt(orientation, direction, up)
    self.cam.setQuat(orientation)
1 Like

No worries :slightly_smiling_face: I think I’ve figured out the issue. I read several other posts on this site and I believe the problem is gimbal lock. I confirmed this by using different ‘up’ vectors and the flipping happened in different orientations, which seems very similar to what other people have described as gimbal lock.

It seems a better solution might be to use quaternions as you suggested. I’ve only recently started teaching myself about quaternions so it might be a while before I figure out how to do it properly :sweat_smile:

Nevertheless, thank you very much for helping me with the initial problem!

I’m guessing that “target” is a NodePath–if so, then the problem would seem to be that Panda doesn’t offer a version of “lookAt” that takes only an “other” NodePath and an “up” vector.

The options listed there that include an “other” NodePath are the following:

look_at(const NodePath self, const NodePath other)
look_at(const NodePath self, const NodePath other, const LPoint3f point)
look_at(const NodePath self, const NodePath other, const LPoint3f point, const LVector3f up)
look_at(const NodePath self, const NodePath other, float x, float y, float z)

Of those, one includes an “up” vector–but it also requires a “point”. (Presumably a target-point relative to the “other” NodePath.)

So, it looks like using “lookAt” in this way can be done, but it would presumably call for the inclusion of a “point”-argument. That said, I imagine that this could simply be a zero-point, indicating a position at the origin of the target NodePath.

(You could also specify your target not as a NodePath, but as a point, as one of the versions of “lookAt” listed above takes just a “point” and an “up”-vector.)

You’re correct about ‘target’ being a NodePath. After reading the API reference, I tried all those various combinations of points, NodePaths and ‘up’ vectors. I even tried setting the target to be a point rather than a NodePath. I was able to get rid of the error, but I haven’t managed to fix the issue of gimbal lock (if that’s what it is).

That’s fair. I was just addressing the error itself, I think–as for gimbal lock, as you say, quaternions may indeed be a preferable solution!

1 Like

So I’ve been doing some reading and I found the perfect solution (Trouble with my spin camera code), which is so much better than whatever I was trying. In short, I just created a dummy node attached to the lander, I reparented the camera to the dummy node and rotating the dummy node gives the exact effect I was looking for. And the best part, no gimbal lock!

1 Like