Camera transforms that never clip through the model

Short story: How does one model a camera system to view or inspect a static mesh such that it never clips through the near and far projection planes, while also allowing a virtually limitless zooming? It appears to have been done in a 3rd party package called “Eyeshot” (free demos are downloadable for anyone who’s curious).

Long story:

I am the graphics programmer at an aerospace design software company, and we are gradually moving to replace a 3rd party graphics solution,“EyeShot” with our own custom graphics in OpenGL, or OpenTK more specifically (now that they have a dedicated graphics programmer - me).

The graphics engine we need is actually quite rudimentary compared to some game engines I’ve worked on, but EyeShot has an uncanny ability to allow camera controls that, far as I can tell, NEVER result in the model being clipped through the near or far planes of the perspective projection.

I am no stranger to creating camera controls, I am able to implement pan/zoom/rotate capabilities that behave with respect to the mouse’s location on the model, that’s no trouble, but how does one prevent near/far plane clipping from ever happening, while allowing seemingly limitless zooming?

The geometry we are rendering is static, its not being animated or moving around the scene in any way.

Zooming approach 1: Scale model matrix, shift camera position.

My initial approach to zooming in by some scale factor k:
1. deduce the view coordinates, v0, of the portion of the model, p0, that lies beneath the cursor,
2. scale the model-view matrix by k, and adjust the camera translation until the same world position, p0, maps to the same view coordinate v0.
In other words, the thing beneath the mouse does not appear to move, despite the change in scale.

Well that works great but after you zoom in far enough, the scale factor k causes the model to pass through either the near or far boundaries and clip them.

Zooming Approach 2: Screen space scale.

The next idea was to simply scale up the size of the rendered image in screen coordinates.

So I put this at the end of my Vertex Shader for mesh rendering:

gl_Position.x *= gScreenSpaceScale;
gl_Position.y *= gScreenSpaceScale;

what I found was that this worked reasonably well for zooming in real close to things without pushing them outside of the near/far planes, but A) it caused weird visual distortions when zooming out because you see more of the mesh that would otherwise be off the edge of your screen, and B) zooming in this manner somehow creates the illusion that your view is rotating slightly. Something to do with focal points/vanishing points, I suspect.

I was able to combine both approaches such that it begins screen space scaling after you’ve zoomed in beyond a certain threshold. Zooming out using the 1st approach is a non issue since it shrinks the model and keeps it safely within the near/far planes.

It occurs to me that maybe I can mathematically determine the vanishing points/focal points or something of that nature that are creating the illusion of rotation with the screen space scale, and maybe I can adjust the camera angle to counteract that illusion, but that’s as far as I got.

Has anyone found an approach for this, or found any good articles on the matter?

You can prevent clipping through the far plane by setting the far distance to infinity (e.g. using glm::infinitePerspective). You can prevent clipping through the near plane by reducing the near distance. Or by simply disabling near/far-plane clipping with glEnable(GL_DEPTH_CLAMP) and using a floating-point depth buffer. With this approach it’s preferable to construct the perspective projection so that Zndc=-1/Zeye, i.e. depth tends to zero as -Zeye tends to infinity. That way, the reduction in depth scale as you get farther from the viewpoint is offset by the increase in floating-point precision as you get closer to zero.

Thank you for your response!

Or by simply disabling near/far-plane clipping with glEnable(GL_DEPTH_CLAMP) and using a floating-point depth buffer.

That sounds like it would handle both the near and far clipping problem together, which is what I need.

Guess I’ll have to research floating-point depth buffers! :stuck_out_tongue: let me know if you have any suggested articles or code demos to recommend.

So I’m not sure I understand this part. In my mind, if this approach prevents clipping from both the near and the far end, then Zeye tending to positive infinity would tend to a Zndc value of 1 and Zeye tending to negative infinity would tend to a Zndc value of 0 (or vice versa depending on what depth function you’re using).

With Zndc=-1/Zeye that would map to 0 for both positive and negative infinity. Can you explain?

Reading some articles about floating-point depth buffers, it seems like they allow you to make the near plane very very small but not to eliminate it entirely.

Ahhh. Upon closer inspection it appears the “Eyeshot” package is using an orthographic projection, so this is a different problem entirely.

Still not sure how they achieve this with no near/far boundary, but I’ll have to reconsider the issue.

Orthographic projections still have near and far planes. The near and far planes are a consequence of the depth buffer typically using a normalised (fixed-point) representation, meaning that you need to decide what the minimum and maximum depth values represent in terms of NDC Z. If you use a floating-point depth buffer, you don’t have to worry about that because you can just store the NDC Z value directly in the depth buffer without any conversion.

Issues of precision are moot because the accuracy of the calculated NDC Z is essentially the same as the accuracy of the initial data (vertex coordinates and transformation matrices).

But for an orthographic projection, you probably don’t need to bother with a floating-point depth buffer. Depth precision is more of an issue for perspective projections because the relationship between eye-space Z and depth follows a non-linear relationship. For an orthographic projection, you can probably get away with a conventional 24-bit normalised depth buffer and just set the near and far planes based upon the minimum and maximum eye-space Z. I.e. if you move the viewpoint or zoom the model, you just move the near/far planes accordingly.