This time we'll finally discuss the techniques used in a Portal Engine. You know from the name of this series that we will be building one of these. So we should really take a close look at its underlying principles: how it works, why it works, and why you'd want to use it. (Actually, a good game engine nowadays shouldn't use only one kind of technique. It should mix and match. So you will see later that our engine uses portal technique to draw rooms -- walls, windows, houses -- and other principles to draw things in rooms -- people, tables, columns. Still later you'll might want to use a landscape engine to draw what you see when you look outside a castle window, for example. The great thing about portal rendering techniques is that they are open to mixing with other techniques.)
Basic Idea:
In a portal engine, the world is divided into sectors. The central idea here
is that these sectors are convex. That means that however you look
at the sector from the inside, none of the polygons overlap each other. For
example, a sphere is a convex sector. The great thing about convex sectors
is that you don't have to sort the polygons or anything. You can draw them
in any order, and they still won't overlap each other. You can probably already
imagine how you could draw different types of rooms as convex sectors.
What happens when you want two rooms? Obviously, when taken together they
don't form a convex sector. This is where you use a portal. Imagine
a portal as being a window. You can see from one room to the next
through the window, and of course you can't see anything not
through the window. A portal has vertices like a polygon, but instead of
a texture being mapped to it, the vertices are used to set up the planes
to "cut" the next room so that only the portion you see through the portal
is rendered.
In the room you're in, none of the polygons overlap each other. The polygon
with the orange outline in the picture is the portal. It clips the room behind
it, then that room is drawn.
And if in the process of drawing the second room you come upon a portal that
hasn't been clipped away, you clip the room behind THAT portal and draw it,
then return to drawing the second room. You keep recursively drawing rooms
through visible portals until there are none left. Then you're done. Think
about this process and understand it well.
Technical Details:
Okay, now... remember the viewing pyramid?

Well, clipping to the portal is exactly like that! Imagine that the polygon
from which you make the planes isn't the screen, but the portal. And the
planes only apply to the room(s) which are rendered THROUGH the portal. (In
fact, in your engine you can have the screen be a portal, so the viewing
pyramid is then the screen portal's frustrum plus the z=0 plane).
The way clipping to a portal works is: you set up a plane for each side of the portal. If a polygon [in the room being rendered through the portal] is made invisible by any of the planes, it is invisible, and you can discard it. Otherwise, it is at least partly visible. A polygon can be clipped by one or more planes, as clipping is performed first by one plane, then by another and another. Finally, you get only the polygons that are visible through the portal, and those polygons are clipped away so that only the part visible through the portal remains. You can now draw those visible, clipped polygons and be sure that you're exactly "filling in" the portal as you should.
A note on hardware acceleration: with hardware acceleration, it is enough to just see if a polygon is visible or not. You don't actually have to spend the extra time to clip the polygon to a portal frustrum. This is because of z-buffering, which automatically takes care of overlapping. Z-buffering on today's 3D accelerators is faster than your clipping algorithm, so you can avoid the extra optimization work when clipping polygons to portal frustrums and simply leave it to z-buffering. But this is only for portals. For clipping to the z-plane, for example, you will still need to actually CLIP polygons. The same normally goes for the entire viewing pyramid. [And for software rendering you still need to clip, because z-buffering in software is slow.]
Okay, so how do we set up the planes for a particular portal? Remember, the portal can have 3 sides, 4 sides, or 10 sides! Consider that, given a triangle, you can create a plane. That is, you can create a plane from 3 points. So let's set up the planes the straightforward way: consider pairs of vertices for the portal, and create a plane from the two vertices and the eye. Makes sense, right? The plane goes from the eye to the two points, and serves to clip the polygons in the room being rendered. (Think of the viewing pyramid.)
The only problem is, the normal vectors for these planes have to face
into the portal, not out of it. This is the case because we are aiming
to leave the polygons that are visible through the portal! When we
create a plane from a triangle its normal vector can face either way,
so we have to choose the normal vector that faces inside. How do we
do this? Well, we can do the following:
when you consider each pair of vertices, for example vtx2 and vtx3,
take the average of the vectors (vtx1-vtx2) and (vtx3-vtx2),
and you obtain a vector that points well inside the portal.
If the angle between the plane's normal vector and the "average inside" vector
is more than 90 degrees, negate the plane's normal vector to make it point
the other way.
Here's an illustration of this:
Alright, now... where should we put portals? Generally you should have in mind large convex sectors for rooms and such. The portals would link these rooms. Think about rooms in Quake... where would you, roughly, put the portals? Don't worry about things in the rooms. We will use another method to render those. The beauty of a portal engine is that you can combine it with other techniques!

Here's a screenshot of a portal engine that I created a while ago. Could
this scene be one convex sector? Where would you put portals? (Hint: between
the edges of walls?)
We can make much more of our portal engine, but that's enough for this tutorial.
We will continue making our engine more interesting in the next tutorial.
But I will stop here, because I want you to get a good understanding of exactly
how a portal engine works. Remember,
what are convex sectors?
what are portals and how can we use them?
how do we clip rooms to portals?
You should be comfortable with these concepts.
| Preview: In the next tutorials we will explore interesting possibilities.
We'll discuss things such as: Being less strict about convex sectors. Mixing portal techniques with other stuff. What 'sector' is the player/camera in? Keeping track of sectors and polygons. After you get the hang of that kind of stuff, it gets very interesting. Because using your knowledge you can implement lighting, reflective surfaces, transparent effects like water, halos, and much more! |
Ah, finally you get to see how to get this stuff working. The suspense is over. You missed The Code section, didn't you? Admit it! Well, you're in luck, because this time the code section is large! And important...
First, how can we define a portal structure? Since it contains vertices like
a polygon, plus some other stuff, maybe we should make a portal structure
contain two pointers to the sectors it links together, and one pointer to
the polygon which defines its vertices -- its position. You can have other
stuff (a later tutorial talks about engine organization) but this seems like
a pretty good definition. Hey, look -- there it is below!
;-)
typedef struct Portal
{
SECTOR *Sec1, *Sec2;
POLYGON *Poly;
int Reserved; // we'll see what this one's for
} PORTAL;
Okay. Now we have a room, and it contains polygons and portals. When we render the room, we will render portals first then the polygons (for reasons covered in later tutorials). So it's good to keep the portals as separate objects in a separate array, since we're going to draw them before polygons. Here is what a sector (synonymous with room) structure contains:
typedef struct Sector
{
PORTAL *Portals; // Pointer to array of Portals
POLYGON *Polygons; // Pointer to array of Polygons
int PortalCount, PolygonCount;
} SECTOR;
(In the engine organization tutorial, we'll find better ways of storing arbitrary numbers of polygons, portals, sectors, rooms, etc... but for now, an array of, say, 100 elements, will be enough.)
Alright, so a sector contains a portal pointer, and a portal contains a sector
pointer. How can this be possible? Well, typedef the sector
and pointer structures beforehand, without elements. Something like
this:
typedef struct Portal PORTAL;
typedef struct Sector SECTOR;
struct Portal { ... };
struct Sector { ... };
If that doesn't work (it is not suported by some older compilers) you can store the index of the sector in an array rather than a pointer to it. The array of sectors will be in the world structure.
Alright, now we have the two structures. How do we implement the drawing
of sectors and portals? Well...
int DrawSector(SECTOR *, CAMERA *);
int DrawPortal(PORTAL *, SECTOR *);
int DrawPolygon(POLYGON *); // implemented in previous tutorial
:)
PLANE *NewPlane(POINT pt1, POINT pt2, POINT pt3);
PLANE *Planes[100]; // an array of clipping planes
int PlaneCount = 0;
int ClipPolygon(POLYGON *, PLANE *);
int ClipPolygon(POLYGON *);
We have these five functions. DrawPolygon has been implemented in a previous tutorial. So was the first ClipPolygon version. NewPlane has been implemented, but this version takes three points rather than a point and a vector. DrawSector will draw all portals and polygons in the sector. We will maintain a "stack" of planes which will be used for clipping polygons (and portals) before drawing them. DrawPortal will add planes to the stack, call DrawSector, and then remove the planes and return. The second version of the ClipPolygon function will clip a polygon by all the planes in the Planes array.
Portals have an integer member called Reserved, which keeps track of whether
the portal has already been drawn. A portal cannot be drawn through itself,
because that would lead to an infinite loop. (The case is a bit different
with mirrors, in a later tutorial.) So in the beginning we test to see if
the portal is already being rendered, by checking if its Reserved value is
TRUE. If it's not TRUE, then we make it TRUE, call DrawSector, then make
the value FALSE at the end and return. A portal can be rendered twice (the
same window rendered through two doors) but it cannot be rendered because
of itself (seeing the window in the window). Take a moment to understand
that. Anyway, that means that when portals are created, their Reserved variables
must be set to FALSE at the beginning.
Here's how we could implement the functions ClipPolygon, DrawSector and
DrawPortal:
int ClipPolygon(POLYGON *Poly)
{
int i;
for (i=PlaneCount-1; i>=0; i--)
{ // go backwards... from most imposing planes to least,
to save work
ret = ClipPolygon(Poly,
Planes[i]);
if (ret == 0) break;
}
return ret;
}
#define CompareCameras(Cam1, Cam2) \
((Cam1).x == (Cam2).x) && \
((Cam1).y == (Cam2).y) && \
((Cam1).z == (Cam2).z) && \
((Cam1).yaw == (Cam2).yaw) && \
((Cam1).roll == (Cam2).roll) && \
((Cam1).pitch == (Cam2).pitch))
int DrawSector(SECTOR *Sec, CAMERA *Cam)
{
register int i;
static CAMERA LastCam;
if (CompareCameras(*Cam, LastCam) == FALSE)
{ // Camera position changed, translate
polygons
LastCam = Cam;
for (i=0; i<Sec->PortalCount; i++)
Wor2Cam(Sec->Portals[i]->Poly);
for (i=0; i<Sec->PortalCount; i++)
Wor2Cam(Sec->Polygons + i);
}
for (i=0; i<Sec->PortalCount; i++)
// process portals
if
(ClipPolygon(Sec->Portals->Poly))
if (Sec->Portals[i]->Sec1 ==
Sec)
DrawPortal(Sec->Portals + i, Sec->Portals[i].Sec2);
else
DrawPortal(Sec->Portals + i,
Sec->Portals[i].Sec1);
for (i=0; i<Sec->PolygonCount; i++) //
draw polygons
if (ClipPolygon(Sec->Polygons +
i))
DrawPolygon(Sec->Polygons + i);
}
int DrawPortal(PORTAL *Por, SECTOR *Sec)
{
int i;
int a, b, c, d;
VECTOR in;
PLANE temp;
if (Por->Reserved) return;
Por->Reserved = 1;
for (j=1; j<=Por->Poly->Count; i++)
{
a = (j-1) % Por->Poly->Count;
b = j % Por->Poly->Count;
c = (j+1) % Por->Poly->Count;
d = (j+2) % Por->Poly->Count;
temp = NewPlane(eye,
Por->Poly->c[a], Por->Poly->c[b]);
in.x = (Por->Poly->c[a].x -
Por->Poly->c[b].x +
Por->Poly->c[d].x
- Por->Poly->c[c].x) / 2;
in.y = (Por->Poly->c[a].y -
Por->Poly->c[b].y +
Por->Poly->c[d].y
- Por->Poly->c[c].y) / 2;
in.z = (Por->Poly->c[a].z -
Por->Poly->c[b].z +
Por->Poly->c[d].z
- Por->Poly->c[c].z) / 2;
if (in.x * temp->a + in.y * temp->b + in.z *
temp->c < 0)
{
temp->a = -temp->a; // reverse
the direction of
temp->b = -temp->b; // the plane's
normal vector
temp->c = -temp->c; // so it points
inside the portal
}
Planes[PlaneCount++] = temp; // Add the clipping
plane
}
DrawSector(Sec); // Draw the sector with the
extra planes set up
PlaneCount -= Por->Poly->Count; // Remove the
clipping planes
Por->Reserved = 0; // Allow drawing this portal
again
}
You can see the recursion in the above code. DrawSector calls DrawPortal, DrawPortal calls DrawSector, until there are no more portals left to draw. Then the scene is done! :)