Projecting a rectangle into screen space

I am trying to implement Doom’s Visportal Occlusion system in Unity: https://www.iddevnet.com/doom3/visportals.php

As the link states, the goal is to perform “unions of the screen space rectangles between the areas”. However, since the rectangles, or visportals, exist in the 3D world, a method of projecting them into the screen space is necessary, and Camera.WorldToScreenPoint method seemed perfect for it.

So first, I was able to achieve this:

The lines are drawn with GL_LINES, in the screen space by using the WorldToScreenPoint method in each of the visportal’s corners.

However, due to how the method works, looking at the portal in different angles makes some corners go behind the camera, producing unwanted results:

In this example, the left bottom corner of the visportal, because it is behind the camera, gets wrapped around, which makes the resulting shape unusable for further calculations.

So how can I bypass this? How can I turn a 3D “Rectangle” in the world and convert it into a 2D version, flattened into screen space?

One solution i can come up with is cutting the rectangle straight by the screen, achieving a shape like this:

But I have no idea how to implement this, especially since rotating the camera makes the conversion tend to infinite…

Any idea?


After receiving such great responses, I decided to add a link for a sample project containing my current implementation. If anybody thinks he can provide with improvements and feedback, feel free!

Link: http://www.filedropper.com/visportal

Basically, each visportal knows both rooms it is linking, and each room knows every visportal it contains.

For now, each room has a trigger collider so that I know where the player is. I do not love this approach, especially since unity’s callbacks aren’t always reliable, but it will do for now.

Then, for each room where player is, I call a recursive method which continuously performs polygon clipping with the help of the Clipper library (http://www.angusj.com/delphi/clipper.php).

If there is a solution, the method shows the room in which the player is not (so if im in room A and visportal connects A and B, I want to show room B), and recalls itself for each portal of the new visible room.

If you approach a portal and rotate the camera, you will see the problem deriving from Camera.WorldToScreenPoint, where a corner will go to the other side, due to the wrapping.

I hope this can be of use to somebody :slight_smile:

Vis portals is a quite advanced technology. As you mentioned Doom3 i took a look at the dev-article you’ve linked and the actual code (since the Doom3 source has been released under GPL).

It’s a bit more complicated than the article might suggest ^^. The article is mainly written for mappers to understand what a visportal is.

The actual portals are defined in worldspace and stay in worldspace. It’s true that they create a screenspace axis aligned rect around the actual polygon, but it’s only used as scissor rect for entities. Since that rect is axis aligned (so it’s an actual rectangle unlike the projected portal borders) the scissor clipping is also axis aligned. You can see this in the last picture in the article where the geometry isn’t clipped by the actual portal lines but by the enclosing AA-Rect.

What actual happens is they use a class they call “idWinding” which is basically just a collection of vertices that form a convex shape. For each portal they create clipping planes for each edge of the winding. Additionally they clip the winding to the parent portal clipping planes which could increase / decrease the point count of the winding. If the point count is 0 the portal isn’t visible.

In addition you should be aware of the fact that portals are “directed”. They only work in one direction. So at an intersection of two areas you always need two portals, one that belongs to room A and leads to room B and one that faces the other direction and belongs to room B and leads to A. Each portal also has a “surface plane” so you can do an easy check if you look at the “right” side of a portal. Portals that face in the wrong direction are ignored.

The clipping code is quite simple and straight forward as the shape always has to be convex. cutting a convex polygon in half always leads to no, one or two intersections. There are two different clipping methods Clip and ClipInPlace. Clip creates a new Winding while ClipInPlace cuts down the current.

If you want to explore the source of Doom3 i recommend you install VisualStudio Express for C++ so you can easily lookup certain types / classes / …

edit

So here we are, finally ^^:

WebGL build of my example

And here’s the whole project as ZIP

It’s not yet in a state that could be used. There are still some minor epsilon errors (an area might be clipped too early if only a small piece of a corner is in view). Also one major problem is, at least in this example: Shadows!. Even an area can’t be seen by the player through any portal, you can still see the shadow which might be casted into a visible area. So when an area is disabled you’ll see the shadow is changing. Doom solves this completely different since the VisPortals are integrated into the whole renderer.

Also at the moment there are a few things that should be changed, depending on the requirements:

  • At the moment an area that isn’t visible is simply “deactivated”. So the whole section just vanishes. This can cause problems with other things like AI characters or other things. It would be better to just disable all renderers in an area instead erasing it from the scene temporarily.
  • Along with point 1 the next thing is the way how the current area is determined is a bit, well, “limited”. I use one of my polygon shapes to test if we are inside an area. However that polygon is a 2d shape flat on the ground. So it doesn’t matter at which height you are, you are always inside the area. The best fix would be to use box colliders to roughly enclose each area, do a Physics.OverlapSphere around the camera and then do the area check for those areas only. That allows you to have areas on top of each other (think about a multiple story building).

Oh, about the WebGL example, you can move around with WASD and you have to hold the right mouse button down to look around. The mouse hiding / locking is a bit odd in WebGL. After you allowed it, it doesn’t propertly lock the cursor unless you press a key ^^. It automatically unlocks when you release the mouse button (pretty much like Unity’s sceneview movement). You also might want to enable the debug draw button.

The mouse speed is really strange in WebGL. If the canvas is small (say 100x100 pixels) the mouse speed is slow. If you have a large canvas it moves extremly fast. At least in FireFox that’s the behaviour i noticed. The actual speed is very low when testing in the editor.

(ps: you can exchange the two views if you like ^^)

I think what’s going on here is your camera’s near distance is cutting off the 3D object of the vizportal so it is literally behind the camera. Try changing the near distance down to 0.

Another way you might consider doing this is with an AABB plane for the camera’s frustum. You can see an example of the AABB frustum checks here: https://www.3dbuzz.com/training/view/3rd-person-character-system

You could also use scripts on the objects with renderers and use Unity’s OnBecameVisible/OnBecameInvisible event (Unity - Scripting API: MonoBehaviour.OnBecameVisible()) to enable/disable the objects.

But if you’re doing this to just optimize occlusion then, Unity should already be handling this for you.

When using viewspace, it’s important to keep in mind, it is NOT a smooth continuous coordinate system like world-space. For example a point at z=0, is technically undefined. (Camera position in Viewport coordinates - Questions & Answers - Unity Discussions)

Another thing I’ve noticed is that: if a coordinate is in front of the camera, at view coord (x,y,+z), and is viewed in the top right side of the screen, then (x,y,-z) will be the LOWER_LEFT side of the screen. If you think about the view frustum, and draw a line along one of the corners, through the camera, and out the back, you can see WHY viewspace does this.

You are right, the code I posted before just wasn’t working as I thought. I’m starting to suspect there may not be enough information in the Vector3 provided by WorldToViewportFunction, to do this with only 1 point (no “w”).

The best I could get working was clipping the line segment at the edge of the screen, in VIEWspace, given two worldspace points.Though I’m not sure if this is actually what you want, or had working already…here is it anyway:

  • Inputs: P1World,P2World, Camera

  • Outputs: P1View,P2View (both will
    have a positive Z > 0)

           Vector3 P1View = cam.WorldToViewportPoint(P1world);
         Vector3 P2View = cam.WorldToViewportPoint(P2world);
         Vector3 lineViewSpace = P2View - P1View;
         if (P1View.z < 0)
         {
             if (P2View.z < 0)
             {
                 //don't draw, both out of view
                 return false;
             }
             //line eq in parameter form
             //x = x0 + t(x1 - x0) 
             //y = y0 + t(y1 - y0)
             //z = z0 + t(z1 - z0)
             //(a-c0)/cDiff=t  (where c is x,y or z)
             float a = 0; // x-left or y-bottom coord
             float t; //param used to compute intersection points
             if (Mathf.Abs(lineViewSpace.x) > Mathf.Abs(lineViewSpace.y))//slope of line is such that it will pass off screen on the left/right side
             {
                 if (lineViewSpace.x > 0) //if line passes off screen on RIGHT
                     a = 1;// x-right coord
                 t = (a - P1View.x) / lineViewSpace.x;
             }
             else//slope of line is such that it will pass off screen on the top/bottom side
             {
                 if (lineViewSpace.y > 0)//if line passes off screen on TOP
                     a = 1; //y-top coord
                 t = (a - P1View.y) / lineViewSpace.y;
             }
             //now that we have t plug into param line form
             P1View = new Vector3(P1View.x + t * (lineViewSpace.x), P1View.y + t * (lineViewSpace.y), nearDist);
         }
         if (P2View.z < 0)
         {  
             //line eq in parameter form
             //x = x0 + t(x1 - x0) 
             //y = y0 + t(y1 - y0)
             //z = z0 + t(z1 - z0)
             //(a-c0)/cDiff=t  (where c is x,y or z)
             lineViewSpace *= -1;
             float a = 0;
             float t; //param used to compute intersection points
             if (Mathf.Abs(lineViewSpace.x) > Mathf.Abs(lineViewSpace.y))//slope of line is such that it will pass off screen on the left/right side
             {
                 if (lineViewSpace.x > 0) //if line passes off screen on RIGHT 
                     a = 1;
                 t = (a - P2View.x) / lineViewSpace.x;
             }
             else//slope of line is such that it will pass off screen on the top/bottom side
             {
                 if (lineViewSpace.y > 0)//if line passes off screen on TOP
                     a = 1;
                 t = (a - P2View.y) / lineViewSpace.y;
             }
             
             P2View = new Vector3(P2View.x + t * (lineViewSpace.x), P2View.y + t * (lineViewSpace.y), nearDist);
         }