NodePath.setMat causes different behavior than .setPos

Hi, I found an oddity in the panda source that causes unpredictable behavior. The problem is that NodePath objects can get different matrices depending on how the state is input (setMat vs setPos). I tracked the problem in the source and believe I know the cause, here it is:

In nodePath.cxx, set_mat() calls transformState’s make_mat() method, which exchanges the input matrix with the Mat4.identMat() when the input and identMat are within NEARLY_ZERO (defined in dtools), here’s the source – in src/pgraph/transformState.cxx:

// Make a special-case check for the identity matrix.
if (mat == LMatrix4f::ident_mat()) {
  return make_identity();
}

This causes unpredictable behavior because the other matrix-modification methods (ie. setPos, etc) don’t enforce this rule. Moreover this rule is sometimes unappealing because it is not always the case that the developer wants matrices that are nearly identity, but not quite, to be made exactly identity – instead it would be preferable to have that be a developer-controlled behavior.

I suggest that in future releases this behavior simply be removed, unless there is some overwhelming reason to keep it.
Thanks! Peter

Here is a python script that exhibits the behavior:

from pandac.PandaModules import NodePath
from panda3d.core import Mat4, Vec3

mat = [1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]
pos = [0., 0., 2.22222222222222222222222e-16]
mat[12], mat[13], mat[14] = pos[:]

vpos = Vec3(mat[12], mat[13], mat[14])
mmat = Mat4(*mat)

print "Variable behavior when setting by matrix vs position\n"

nodeM = NodePath('testM')
M0 = nodeM.getMat()
print "init matrix's z-coord:\n", M0[3][2]

nodeM.setMat(mmat)
M1 = nodeM.getMat()
print "setMat's resultant z-coord:\n", M1[3][2]

nodeP = NodePath('testP')
M2 = nodeP.getMat()
print "init matrix's z-coord:\n", M2[3][2]

nodeP.setPos(vpos)
M3 = nodeP.getMat()
print "setPos's resultant z-coord:\n", M3[3][2]

print '\n\nVariable behavior when matrix is nearly identity or not\n'

mat2 = mat[:]
mat[0] += 4.17e-7  # adding this keeps mat "nearly" identity
mat2[0] += 4.18e-7   # adding this makes mat not "nearly" identity

vpos = Vec3(mat[12], mat[13], mat[14])
mmat = Mat4(*mat)
vpos2 = Vec3(mat2[12], mat2[13], mat2[14])
mmat2 = Mat4(*mat2)

nodeM = NodePath('testM')
nodeM.setMat(mmat)
M0 = nodeM.getMat()
nodeM.setMat(mmat2)
M1 = nodeM.getMat()
print "setMat's resultant z-coords, nearly/not-nearly identity:\n", M0[3][2], M1[3][2]

nodeP = NodePath('testP')
nodeP.setPos(vpos)
M2 = nodeP.getMat()
nodeP.setPos(vpos2)
M3 = nodeP.getMat()
print "setPos's resultant z-coord, nearly/not-nearly identity:\n", M2[3][2], M3[3][2]

It’s a good point; and it is unfortunate that it was a source of confusion, but there is (IMHO) a good reason for the difference in behavior.

When you use setPos(), you are setting the position (and hpr, etc.) componentwise, which is inherently a more precise operation than setting it matrixwise. The components are stored and manipulated individually to the extent possible. However, when you use setMat(), then only the relatively imprecise 4x4 matrix must be stored. Each consecutive operation with another 4x4 matrix results in a further reduction in precision.

Of course, in the case of a pos value, there’s no actual difference in precision, at least not in the source matrix itself, since the pos is stored directly in its own row of the matrix. The only real difference is when you’re talking about scales, rotations, or shears; in which case there is a substantial difference in precision between the matrix representation and the componentwise representation. There could be an negative effect on pos precision once you start chaining together multiple matrix operations, though. In any case, the general Panda philosophy is this: if you’re using a 4x4 matrix, we assume you’re not as concerned about precision as if you’re using individual components.

The reason for the special-case handling of identity matrix is as an important optimization. If the matrix you’re storing really is meant to be the identity transform, then it’s much more efficient to store a pointer to the global identity transform than to duplicate the identity matrix in a new transform object. And, in fact, due to numerical imprecision, it is quite easy to produce a matrix that is almost, but not exactly, identity, even though it should be be exactly identity based on the math involved; and that is the reason for the NEARLY_ZERO comparison.

I do appreciate that sometimes you really do want to store a very small value directly into a matrix, despite the above. And we could provide a config variable that would disable this default behavior and allow you to store the value as given. But that would add a bit of overhead, and wouldn’t necessarily reduce confusion (you would still have to learn about this config variable somehow, and until you did, you’d likely have to walk the same path you’ve already walked to understand the strange behavior).

But now that you know that setPos() behaves a little differently from setMat() for very small numbers, perhaps you can avoid this problem for your own needs by using setPos(), or by using TransformState.makePos(), instead of using a 4x4 matrix.

David

Thanks, that is very helpful – I’ll probably shift away from rotation matrices to quaternions.

Thanks again