Frustum clipping is a vital, yet neglected subject. I haven't read any documents on it yet on the web. So, now I will show you my findings on the subject, which should prove to be enough for you to implement frustum clipping in your engine.
To clip the polygon, I will be using an algorithm known as the Sutherland-Hodgman polygon clipping algorithm. Its pretty simple to implement, and easy enough to understand.
The algorithm works by clipping a polygon against n planes, one after the other. The output from one clip is used as the input to another. Imagine you have a square piece of wood, say NxM units in size. Now on top of that piece of wood, you put a shape, say a triangle, made of paper. The most obvious way to clip it to the square would be to work your way around the edges, cutting off excess paper as you go. With me so far? So, you would clip against the lines:
(0, 0) - (N, 0) (N, 0) - (N, M) (N, M) - (0, M) (0, M) - (0, 0)Recalling that the square was NxM units in size. This is effectively 2D clipping to the 2D viewport. However, in 3D, we need to clip to a frustum. A frustum is basically a 3d pyramid, with the top lopped off. We store and clip against either 5 or 6 planes, depending on whether you want back plane clipping. The frustum is a convex volume in space, and we can also use it to check whether or not an item is viewable.
There are 4 cases that could occur when clipping the shape. These are:
The calculation in itself is easy enough to do. Its linked to rays. A ray is defined as:
Origin + t*DirectionOrigin will be point 1 on our line, Direction will be (Point2 - Point1). t is a parameter that specifies a point on the line. For this code, t will be in the range 0..1.
t is easy enough to calculate. Firstly, we find the distance travelled in the same axis as we want to clip, for example if we are clipping against Z, then we find Dz, distance-Z. The same could be done for X or Y. Now, if this value is zero, then t is equal to clipping-limit - point1. For example zminclip - point1.z. If dz is not zero, then we take zminclip - point1.z, and divide it by the distance-z. So t now tells us the position of the line we are clipping against, the amount of the line that is behind the clipping limit.
Now calculate similar distance values for x and y. So dx = p2.x - p1.x, and the same for y. Recall the equation for a ray:
Origin + t*DirectionIts now a piece of cake to calculate the missing values! Simply write:
NewX = p1.x + t*dx NewY = p1.y + t*dyAnd theres your clipped point! You can perform similar clippings for u,v, gouraud colour, phong lighting map co-ordinates, anything. But one word of warning: Beware of 1/n values. They can easily underflow. So clip in n values, then take the reciprocal of them. I found this out the hard way, with perspective texturing. After a certain point, the texture would just go mad, swimming and sliding and distorting all over the place. Clipping to u, v then taking 1/u, 1/v solved this problem
The above code will work fine for straight clipping boundaries. But in 3D engines, where we use a perspective projection, the view volume is not flat. It's sloped. This is a problem. Theres two ways around it:
i = xd -- + xcentre zThen your clipping limit for a given z would be:
(+/- xcentre)*z --------------- dA similar thing can be done for Y. But, this is a less elegant solution; we have made special cases, which makes our code more complex, and harder to maintain/code/debug. What we need is a general solution, to clip a line against any arbitrary plane. And such a thing can be done!
If you don't yet know, the equation for a plane is:
Ax + By + Cz + D = 0Where A B C D are the co-efficients of the plane, and xyz is a point on the plane. Recalling that the equation of a ray is:
Origin + t*DirectionIt becomes a case of substituting in the ray equation for x y and z, then re-arranging to give t. The solution is:
(A*origin.x + B*origin.y + C*origin.z + D) t = - ------------------------------------------ (A*dir.x + B*dir.y + C*dir.z)If the denominator (lower half) of the above equation is zero, then the line and plane are parallel; they never touch. Obviously, don't try to clip parallel lines and planes. Your code shouldn't even try to do that. So now we have t, ready to calculate the point of intersection
But how do I find what side of a plane a point is one? Easy! The plane equation. Feed the points points X Y and Z into the equation
Ax + By + Cz + D(yes, the plane equation). If the value is zero, the point is one the plane. If the value is > zero, then the point is on the side of the plane pointed to by the planes normal. If the value is < 0, then the point is on the other side. Ok, to visualise this, do the following. Go into your back garden. Get a pole, a stick, whatever, and stick it in the ground. This is your plane normal. Imagine that is points to the sun. Now, the sky is the area pointed to by the planes normal. Say this plane is your lower Y clipping limit (the bottom of the screen). So if your value is > 0, then you're inside the view volume. If you value is < 0, you're outside, and need to be clipped. Simple eh?
Now you're ready to code it. The following pseudo-code should explain how to implement the algorithm:
void ClipToPlane(POLYGON polyin, POLYGON polyout, PLANE plane) { curvert = 0 vert1 = polyin.numpoints - 1 for(vert2 = 0 to polyon.numpoints) { classify vert1 and vert2 with respect to plane if(both are inside) { insert(vert2) in polyout[curvert] curvert++ } if(entering) { Calculate t Calculate new vertex insert(newvertex) in polyout[curvert] curvert++ } if(leaving) { Calculate t Calculate new vertex insert(newvertex) in polyout[curvert] curvert++ insert(vert2) in polyout[curvert] curvert++ } vert1 = vert2 } polyout.numpoints = curvert polyout.points[curvert] = polyout.points[0] }You'll need to fill in the blanks. Then, you could store a set of clipping planes in an array, then just feed the planes to the above code one after the other.
Seems a lot of you were puzzled when I told you my clipping system didn't need to allocate memory on fly, make new polygons etc... well, to clarify how I do it, I'll briefly describe the way its done in my engine.
Firstly, some notes: this system doesn't generate extra triangles to be sorted, it doesn't allocate memory at runtime, and it projects triangles *before* clipping. Heres how it works:
Bit # | Plane ------+------ 0 | Front 1 | Back 2 | Left 3 | Right 4 | Top 5 | Bottom
You don't need to use this exact scheme, anything will do -- but bear in mind that this will be used in a Sutherland-Cohen style marker. You have 1 byte for each edge of the triangle, and using logical ORs and ANDs, you can determine whether or not the triangle is within frustum. Look into line clipping algorithms for more info on this. Key points are:
When this is done, insert visible triangles into a buffer of pointers to triangles: Triangle *tribuffer[MAXTRI] or as I use it Triangle **tribuffer, and tribuffer is allocated at init-time:
tribuffer = (Triangle *) malloc(sizeof(Triangle *) * max_tris_in_scene);This buffer is then sorted back->front or front->back, and then passes each triangle to a rendering function. Now I project all my vertices, perform lighting etc..
The rendering function is not called directly; instead I have a global function pointer, which points to the current routine to use, for the current rendering system. This way I can switch rendering types on the fly, without having things like switch(rendertype) or big if--then trees. Quite nice...
Firstly this copies in data that may be needed to render the triangle, such as uv co-ords etc into a structure which is passed to the triangle painter. The structure is just Vertex[3], same structure as is used for object verts. Just my uv co-ords etc are stored in triangle structure, so they need to be copied. Also u/z v/z are calculated: u/z = u*(vert->1/z) etc...
Anyway the render function just checks the clip flag: if no clipping, it just calls DrawTriangle(blah) or something, depends on your routine + engine
If it does need to be clipped however, it calls a clipping routine, for the related rendering mode. Eg if rendering for texture, it calls ClipTexture, for gouraud, it calls ClipGouraud, and so on. This routine looks something like:
void ClipTriangle(Triangle *tri) { PlaneClip(tri); for(n=0; n<numclipvert-2; n++) DrawTriangle(outvert[0], outvert[n+1], outvert[n+2]) }
PlaneClip is just a standard Sutherland-Hodgman plane clipper. It is used for all clipping of triangles. "Wait a minute", I hear you say. Thats inefficient, you're clipping gouraud, texture etc, when you might not need it. Wrong. PlaneClip relies on *another* function pointer, which points to a routine that calculates a new point, given two endpoints of a line, and t, point on line. It writes to a pointer that is also passed to it. Clipfunc just looks something like:
void ClipLine(Vertex *vert1, Vertex *vert2, Vertex *outvert, Real t) { *outvert = vert1 + t*(vert2 - vert1) ProjectVertex(outvert) // project to 2D }Obviously I don't have a function call to project the vertex to 2d inside ClipLine, thats done inline. Y'see this allows me to project new vertices as they are created, without having to malloc new ones as I go along, or have a big pass where I project them all. [Tip] when projecting your vertices, and you're sitting waiting for fdiv to complete, try pre-warming your cache. Works very nicely...
Also I can now use one routine to do all my clipping, by using function pointers I can create a psuedo-self-modifying-code type system. All these pointers are set beforehand, with a function called something like SetRenderType, eg SetRenderType(RENDER_GOURAUD_TEXTURE).
Understand now? Its quite a complex system, very difficult for me to explain, difficult for you to understand. To really understand this, you need to be able to see my whole rendering pipeline, which I'm not going to give away right now ...
Rejecting polygons that are not inside the frustum is easy enough to do. If you place a bounding sphere around a polygon, or indeed a polyhedron, or any polytope in 3D, you can do a simple check to see whether or not it is inside the viewing frustum. What you do is simple: Find the distance from the plane to the centre of the sphere. Now, if this distance is < 0, and greater than the radius of the sphere, ie (dist > -radius), then you can chuck the volume out. It only needs to be outside of one plane to be discarded. Bounding boxes are also pretty easy to do. You can just check the 8 vertices of the box, and see if at least one of them is on the positive side of the plane. Or, you can check the diagonally opposite corners, eg the top left and bottom right corners, etc. That way you can get away with less checks, if you're clever.
I hope this web page has provided you with enough information for clipping polygons against planes, specifically the viewing frustum. If you have any questions, mail me. [References: Computer Graphics, Principles + Practice]