coin3d / soqt

Old Coin GUI binding for Qt, replaced by Quarter
BSD 3-Clause "New" or "Revised" License
23 stars 17 forks source link

Possible wrong calculation in So@Gui@ViewerP::setClippingPlanes(void) #70

Open ytterx opened 2 years ago

ytterx commented 2 years ago

We recently found a problem with clipping of the camera with a very small object in SoQtExaminerViewer Larger objects were fine.

To fix this issue, we've implemented the use of setAutoClippingStrategy with a So@Gui@AutoClippingCB function. In this function we do the calculation differently to what is defined in So@Gui@ViewerP::setClippingPlanes(void).

Reason is that the nearval and farval get set to just the box.getMax()[2] and box.getMin()[2] values, while the other array indices also contain values?

So@Gui@ViewerP::setClippingPlanes(void)

  // Bounding box was calculated in camera space, so we need to "flip"
  // the box (because camera is pointing in the (0,0,-1) direction
  // from origo.
  float nearval = this->camera->nearDistance.getValue();
  float farval = this->camera->farDistance.getValue();
  if ( !xbox.isEmpty() ) {
    SbMatrix cammat;
    SbMatrix inverse;
    this->getCameraCoordinateSystem(this->camera, this->sceneroot, cammat, inverse);
    xbox.transform(inverse);

    SbMatrix mat;
    mat.setTranslate(- this->camera->position.getValue());
    xbox.transform(mat);
    mat = this->camera->orientation.getValue().inverse();
    xbox.transform(mat);
    SbBox3f box = xbox.project();

    nearval = -box.getMax()[2];
    farval = -box.getMin()[2];
  }

I'm not that good in 3D graphic calculations, but this seems wrong

veelo commented 2 years ago

I haven't looked into this problem, but I can try to explain what the intention of this code is.

In order to correctly render partly overlapping objects, i.e., where some of the polygons obscure (parts of) other polygons, every pixel is given a depth value, i.e., the distance from the observer. Pixels with a lower depth value overwrite pixels with a higher depth value, for the same (x, y) on the screen. In the camera coordinate system, the depth is measured along the z axis (parallel to the viewing direction). This information is stored in the z-buffer, alongside the pixel-buffer.

Because the precision of floating point values in computers is limited, you cannot let depth values run from minus infinity to positive infinity. This is where the near and far clipping planes come into play. These are imaginary planes perpendicular to the viewing axis, one near to the camera and one far from the camera, and they define the range of the depth values. Every pixel that falls in front of the near clipping plane is not rendered, and (I think) every pixel that falls behind the far clipping plane ends up with the same maximum depth value (or is ignored, I am not sure).

If the clipping planes are very far from each other, e.g., if the scene contains a close object and a wide mountain range in the background, polygons that overlap but are close to each other, can end up with the same depth value because of lacking precision. This can cause flicker effects in the rendering. So in general, you want all geometry to fall in between the two clipping planes (except that it doesn't make sense to put the near clipping plane behind the camera) but otherwise keep them as close as possible, to maximize the depth resolution. That is why only the second index (z value, depth in viewing direction) of the bounding box extremes is taken into account.

If your scene only contains a very small object not very far from the camera, or objects are close to each other in relation to their size, there should be no issues. As you don't describe what happens or provide an example, I don't know what the problem could be. If the small object is very far from the camera, so that the near and far planes have a very high z value and only differ minutely, the precision of floats might actually be insufficient, causing the clipping planes to actually clip the geometry. This might go unnoticed especially with orthogonal cameras. In that case maybe you can bring the camera closer to the object.

You don't share your alternative calculations either, so I have no idea why they might work better in your case, but I suppose the planes end up at a reasonable place and contain the object. They might be wasting depth resolution though, I don't know.

I hope this is helpful information so you'll be able to understand what is going on. If the code in question can be improved, then by all means please share it with us :-)

Cheers, Bastiaan.

VolkerEnderlein commented 2 years ago

I just had a look into the original SGI OpenInventor implementation here. It seems there is a different interpretation in what the getXfBoundingBox() function returns for the sceneRoot. If you could provide a small iv file or a minimal reproducible example that would be very helpful to rule out the other reasons @veelo already mentioned. Cheers, Volker

ytterx commented 2 years ago

I'll try to find some time in the coming week to create a minimal reproducible case.

The "solution" that seems to work for now:

void MyViewer::setClippingNearVal(float nearval)
{
   if (m_nearval != nearval)
   {
      m_nearval = nearval;
      setAutoClippingStrategy(SoQtViewer::CONSTANT_NEAR_PLANE, m_nearval, fixedDistanceClipPlanesCB, this);
   }
}

SbVec2f fixedDistanceClipPlanesCB(void * data, const SbVec2f & nearfar)
{
   // Get the camera position
   const SbVec3f cameraPosition = ((MyViewer *) data)->getCamera()->position.getValue();

   // Get the boundingbox of the scene
   SoNode* scene = ((MyViewer *) data)->getSceneGraph();
   SbViewportRegion vp( ((MyViewer *) data)->getViewportRegion() );

   SoGetBoundingBoxAction bboxaction(vp);
   bboxaction.apply(scene);

   const SbBox3f boundingBox = bboxaction.getXfBoundingBox().project();

   // Get the closest point to the camera on the boundingbox
   const SbVec3f pointOnBox = boundingBox.getClosestPoint(cameraPosition);

   // Calculate the minimum distance between the camera and the boundingbox
   // Then multiplying by 0.1 to get a good value for clipping.
   float nearval = getDistance(pointOnBox, cameraPosition) * 0.1;
   ((MyViewer *) data)->setClippingNearVal(nearval);

   return nearfar;
}

Where the class MyViewer is an SoQtExaminerViewer and m_nearval a member variable to not keep setting the setAutoClippingStrategy over and over again..

veelo commented 2 years ago

Several peculiar things going on here. First a nearval is calculated that looks sensible, and reducing that by some small factor seems sensible too, but taking only 10% is so little that it takes away any confidence that the value is at all correct. I would expect a factor of 0.99 or 0.98, not 0.1.

Then the nearfar argument to fixedDistanceClipPlanesCB is not used or touched inside that function, only returned. That seems odd. And it looks to me that there is an infinite loop between the two functions that is only broken by the condition in setClippingNearVal(). That works, but it isn't pretty :-) Again I haven't studied the relevant Coin code, but if this reflects how Coin does things, my eyebrows are still up.

The fact that a very conservative near value solves your problem strengthens my suspicion that floating point precision is at the heart of this issue. Again I would investigate whether very small values are subtracted from very large values, and significant digits are lost.

luzpaz commented 1 year ago

Any progress on this ?

ytterx commented 1 year ago

Hi, sorry not from my/our end, unfortunately it is not a high priority for us, so allocating some time from my end is a bit hard to justify. I'll see if we can re-evaluate this so that we might be able to spend some time on it.

foobarbecue commented 9 months ago

So I'm running into a problem that I think might be related. Autoclipping is working fine with my perspective camera, but when I switch to ortho, objects behind the camera are visible. We are using setAutoClippingStrategy(SoQtViewer::CONSTANT_NEAR_PLANE, 0.05f)

foobarbecue commented 9 months ago

Filed https://github.com/coin3d/soqt/issues/76 since I think mine is a bit different.