Near Plane Clipping
If you've ever messed around with 3D rendering in Desmos, you almost certainly will have encountered this issue:
What's Causing This? #
This problem is caused by the renderer trying to render vertices which are behind the camera. Due to the perspective projection, the vertices' X- and Y-coordinates are divided by their Z coordinates (multiplied by -1 to keep the coordinate system right-handed) to make the triangles appear smaller and closer to the center when they are far away. When the Z coordinate is the wrong sign, the X and Y coordinates' signs are flipped when they are divided, causing them to appear on the opposite side. When vertices in a triangle have different signs, this effect causes massive triangles to tear across the screen. And really, we shouldn't have anything rendering like this in the first place.
How do we fix it? #
The solution to this problem is to implement Near Plane Clipping. The Near Plane is a 3D plane close to the front of the camera which is usually parallel to its surface. When rendering, any geometry that goes past the near clipping plane does not appear. A reasonable near plane in most Desmos-based 3D renderers could be defined by the equation , since a value of -0.05 represents something 0.05 units in front of the camera.
A simple solution #
The conceptually simplest way to apply a technique like this would be to delete any triangle with a Z-value greater than that which the near clipping plane defines as the cutoff. However, many of you can probably already anticipate the problem with this: If only one vertex of a triangle goes past the cutoff, the entire thing is going to disappear abruptly! We could mitigate this by decreasing the opacity of nearby triangles, but even this is a bandaid solution, albeit a nice one if you're strapped for time. We can do better.
Pixel-Perfect Near Plane Clipping #
To get Near Plane Clipping working properly, we have to consider what the Near Plane is even doing to the polygons. In a way, it's a bit like a knife, cutting through polygons and discarding the half that's closer to the camera.
How does it do this? There are four important cases here:
- All of the vertices are behind the near clipping plane, and thus the entire triangle is visible.
- One of the vertices is in front of the near clipping plane and the other two are behind it, creating a new quadrilateral.
- Two of the vertices are in front of the near clipping plane and the third is behind it, creating a new triangle.
- All of the vertices are in front of the near clipping plane, making the entire triangle invisible.
Which vertices are in front of the near clipping plane? #
How do we calculate what case to pursue? Let's assume that we've defined three variables for the X, Y, and Z positions of the vertices in a triangle. We'll call them , , and . They are all three-element lists. We'll also define a variable which is equal to the cutoff value for our near clipping plane. As a reminder, any value greater than is to be clipped.
Any vertex can be either in in front of or behind the near clipping plane, giving us two possibilities per vertex, with three vertices total. This gives us possibilities in total. It'd be annoying to hardcode all of these, so is there a way to generalize this?
One possible solution is to use list filters. With the filter , we can get a list of all the X-coordinates that are behind or on the near clipping plane. Similarly, we can use to get all X-coordinates in front of the near clipping plane. We can then rinse and repeat with the rest of the axes to get filtered lists of points in front of and behind the near clipping plane. But more importantly, we can use one of these filtered lists to decide how many points are in front of the near clipping plane and therefore figure out whether we need to use Case 1, Case 2, Case 3, or Case 4.
Cases 1 & 4 #
Cases 1 and 4 are the simplest: For 1, we render the triangle normally, and for 4, we disregard it entirely, rendering an empty polygon with no vertices instead. Cases 2 and 3 are where things get weird.
Case 3 #
Finding the new vertices #
Let's start with case 3, since triangles tend to be easier to work with than quads, and 3 produces a triangle. The difficulty comes in the fact this triangle isn't our original triangle: Two of its vertices are entirely new, and we need to find out where they're located. First, some definitions of the vertices of the original triangle (pre-clipping) to avoid confusion:
-
Let's call the coordinates of the first vertex in front , , and .
- Altogether I'll call this the "first near vertex"
-
Let's call the coordinates of the second vertex in front , , and .
- Altogether I'll call this the "second near vertex"
-
Let's call the coordinates of the first vertex behind , , and .
- Altogether I'll call this the "far vertex"
With all of this said, the two new vertices are defined as follows:
- The point on the line segment connecting the first near vertex and the far vertex where .
- The point on the line segment connecting the second near vertex and the far vertex where .
Let's consider the first of these points— the one on the segment connecting the first near vertex and the far vertex. We can represent this line segment as a 3D parametric equation where ranges from 0 to 1:
Then to find the -value where , we need to solve the equation for after setting it equal to :
Now that we have , we can plug it back into the other two equations in the parametric to get the X and Y coordinates of the first new point.
We then repeat the process for the other point. This is essentially the same operation, but with one of the endpoints of the line segment changed.
And now that we have these two points, we can connect them together with the far vertex and render the resulting triangle.
Case 2 #
The process for Case 2 is virtually identical. I'll let you derive it on your own, with the help of this picture.
Just make sure to render the points of the quadrilerateral in the correct order, otherwise the quadrilateral will look like this, which you don't want:
The End Result #
Here is a full implementation of near plane clipping as described above. Note that the clipping has been exaggerated here for effect— in your graph it should probably be a lot more subtle (consider a smaller cutoff, maybe 0.1 or 0.05):