Monthly Archives: August 2013

Let’s Talk About Cameras: Perspective and Field of View

[Note: This is technically part of the series on DirectX, but is readable on its own, and does not require programming experience to be understood.]

At the end of my previous post on rendering 3D scenes with DirectX, you were able to render an object, but you would only see a specific narrow area (-1 to +1 in X and Y, 0 to 1 in Z). Well, that’s because we didn’t set up a camera into the scene. We can do that! But first, we need to know how cameras work.

Renderers like DirectX only really care about three properties of your camera. The first two properties are its position and its orientation (where it’s looking at); it’s pretty easy to visualize how changing those parameters will change the final rendered scene. The third camera property that DirectX cares about is the camera’s field of view, and that’s actually pretty tricky for people to visualize. So that’s what this article is about!

Let’s get started! Here’s an overhead view of a living room scene:
Living Room Top Down View

And here’s that same scene viewed from a camera at the front of the living room:
70 Degree FOV Camera Front

And here’s a fun fact!

In the top-down view of this scene, the sofa at the back appears five times wider than the black chair at the front (it’s 302 pixels long, the black chair is 54 pixels long). However, in the above image, the sofa appears only twice as wide (355 pixels long, while the black chair is 180 pixels long). So, the black chair has grown to be 2.5x larger than the sofa!

Of course, that’s not true. Your mind instinctively understands that it’s the same scene, and the same furniture — the chair didn’t really grow bigger. The sofa only appears to be smaller because it’s further away from the camera.

That decrease in size with distance is called perspective, and it can be controlled. You can change how quickly things get smaller, and it creates different images. Check out this picture of the same scene, and hover your mouse over it to compare it with the previous image:


That image above is the same living room scene, except things get even smaller as they get farther from the camera. None of the chairs have moved between these two images.

So how do we describe the degree to which things in a scene get smaller as they get further away? It’s called field of view (or focal length if you’re working with real-world cameras, but I think “field of view” is a clearer term).

Field of view is measured in degrees, and it represents how much of the scene the camera can “see”. If a camera has a 70 degree field of view, then it can “see” anything at most 35 degrees to the left or right from the direction it’s facing. Hopefully, this image will explain things:

Two Different Fields of View

On top, we can see a birds-eye view of the living room with two wedges marking what two different cameras can see. On bottom is the image each camera produces. The camera on the left has a 70 degree field of view, meaning that the triangular visibility wedge in the bird’s-eye view is 70 degrees wide. The camera on the right has a 90 degree field of view, so its wedge is 90 degrees wide.

When the camera’s field of view increases, the camera can see more stuff. However, it doesn’t see equal amounts more nearby stuff and far-away stuff — it sees way more far-away stuff than nearby stuff. Check out how the visibility wedges for the two cameras are almost the same at the nearby black chair, but the 90 degree FOV camera on the right has a massively wider view wedge at the far-away sofa. In order to fit all that far-away stuff, things that are far away have to get smaller. And that’s perspective!

One final note — you may have noticed that the 70-degree FOV camera on the left has actually been placed farther from the black chair than the 90-degree FOV camera on the right. I did this on purpose to make the black chair appear equally big in both scenes. Here’s what it looks like to have the camera increase its field of view and move closer at the same time:

fov-translate

It looks like the world is growing more distant around the chair!

Movies use this effect way often, because it’s a super-fun way to express an idea to the audience.

Alfred Hitchcock’s Vertigo moves the camera back and increases the field of view here to make it look like the ground is receding, emphasizing the main character’s vertigo.

Steven Spielberg does it here in Jaws to show the main character — who just spotted a shark — losing track of everything else around him. Martin Scorsese’s Goodfellas does this trick in reverse (move the camera further away and decrease the field of view) here. This causes the viewer to feel the characters’ paranoia — the world around them is closing in, and they have to watch for an assassin in every corner.

So that’s pretty cool! You just learned a ton about cameras and it came with a free lesson in film appreciation. And come DirectX part 5, we’ll learn how to write our own 3D cameras and play with this stuff ourselves!

DirectX Part 4: Loading FBX Models

“Hey Ben”, you say, “these articles on DirectX are great!” (I know.) “But when you talked about vertices in your previous post, you had us hard-code an array of vertex information. I’m working with snooty artists who want to to use a graphical user interface to place thousands of triangles. Can I import a mesh from Maya?”

Fiiiiiiiiiiiiiiiiiiine.

Some AAA shit right here

Some AAA shit right here

Chances are, your artists save out to .FBX files. FBX is a proprietary format, owned by Autodesk (the guys who make Maya and Max — the tools 90% of game artists use to generate content). If you don’t have any tools/skills to create FBXes, you can download a shitty FBX of a squashed bell that I made (see a screenshot of bell in Maya viewport on the right).

Anyhow, so Autodesk are assholes, but they aren’t stupid, and they know nobody will use their formats if their formats aren’t easy for people to do stuff with. So they created an SDK for loading FBX files and giving you all the sexy data however you want it. You can download the SDK here and you can read the documentation here. It will give you libraries to compile against (probably located in C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\lib\vs2012\x64\release, with headers in C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\include) that parse FBXes into data. It’s free and you can use it in commercial products.

To get the FBX SDK set up in Visual Studio:

  • Download and install the SDK
  • In Visual Studio, right-click on your project > Properties > Configuration Properties > VC++ Directories > Include Directories, and add C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\include . Make sure you do it across debug+release configurations.
  • Right-click on your project > Properties > Configuration Properties > Linker > General > Additional Library Directories, and add C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\lib\vs2012\[x86/x64]\debug for debug configuration, C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\lib\vs2012\[x86/x64]\release for release configuration
  • Right-click on your project > Properties > Configuration Properties > Linker > Input, and add libfbxsdk.lib under “Additional Dependencies” (across debug+release!)
  • Copy C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\lib\vs2012\[x86/x64]\[debug/release]\libfbxsdk.dll into your project’s output directory for the appropriate x86/x64 and debug/release build.
  • You’ll probably want to add a reference to the .pdb containing debug information for the FBX SDK, C:\Program Files\Autodesk\FBX\FBX SDK\2014.1\lib\vs2012\x64\debug\libfbxsdk.pdb, via Tools > Options > Debugging > Symbols

Now that you’ve got access to the FBX SDK, you’re gonna write some code that looks like this:


#include <fbxsdk.h>
#include <vector>

struct MyVertex
{
   float pos[3];
};

FbxManager* g_pFbxSdkManager = nullptr;

HRESULT LoadFBX(std::vector* pOutVertexVector)
{
   if(g_pFbxSdkManager == nullptr)
   {
      g_pFbxSdkManager = FbxManager::Create();

      FbxIOSettings* pIOsettings = FbxIOSettings::Create(g_pFbxSdkManager, IOSROOT );
      g_pFbxSdkManager->SetIOSettings(pIOsettings);
   }

   FbxImporter* pImporter = FbxImporter::Create(g_pFbxSdkManager,"");
   FbxScene* pFbxScene = FbxScene::Create(g_pFbxSdkManager,"");

   bool bSuccess = pImporter->Initialize("C:\\MyPath\\MyModel.fbx", -1, g_pFbxSdkManager->GetIOSettings() );
   if(!bSuccess) return E_FAIL;

   bSuccess = pImporter->Import(pFbxScene);
   if(!bSuccess) return E_FAIL;

   pImporter->Destroy();

   FbxNode* pFbxRootNode = pFbxScene->GetRootNode();

   if(pFbxRootNode)
   {
      for(int i = 0; i < pFbxRootNode->GetChildCount(); i++)
      {
         FbxNode* pFbxChildNode = pFbxRootNode->GetChild(i);

         if(pFbxChildNode->GetNodeAttribute() == NULL)
            continue;

         FbxNodeAttribute::EType AttributeType = pFbxChildNode->GetNodeAttribute()->GetAttributeType();

         if(AttributeType != FbxNodeAttribute::eMesh)
            continue;

         FbxMesh* pMesh = (FbxMesh*) pFbxChildNode->GetNodeAttribute();

         FbxVector4* pVertices = pMesh->GetControlPoints();

         for (int j = 0; j < pMesh->GetPolygonCount(); j++)
         {
            int iNumVertices = pMesh->GetPolygonSize(j);
            assert( iNumVertices == 3 );

            for (int k = 0; k < iNumVertices; k++)             {                int iControlPointIndex = pMesh->GetPolygonVertex(j, k);

               MyVertex vertex;
               vertex.pos[0] = (float)pVertices[iControlPointIndex].mData[0];
               vertex.pos[1] = (float)pVertices[iControlPointIndex].mData[1];
               vertex.pos[2] = (float)pVertices[iControlPointIndex].mData[2];
               pOutVertexVector->push_back( vertex );
            }
         }

      }

   }
   return S_OK;
}

Although a lot of code, it’s not that bad! Here’s what it does:

  • Initialize the FBX loader and tell it what types of data to load (this is the FbxIOSettings, and you can use it to specifically load only meshes, only meshes + materials, etc.)
  • Load the file at C:\MyPath\MyModel.fbx and grab its root node (the “handle” to the FBX contents)
  • Go through each item in the FBX, and ignore any items that aren’t meshes
  • Make sure that every polygon in the mesh is a triangle! This is important because DirectX only draws triangles. Tell your artists to triangulate their meshes (or write something that auto-triangulates meshes here).
  • Copy over the data for the 3 vertices that make up the triangle into your pOutVertexVector

And once it’s all done, you can call LoadFBX(...), pass in a pointer to an empty vector, and you’ll come out with a vector of vertex positions!

Now, I’m leaving it to you to hook everything up. No code samples. It’s your chance to fly solo. Just remember these things!

  • Since you stored your vertex data in a std::vector, everything is contiguous memory. If you copy in myVertexVector.size() * sizeof(MyVertex) bytes starting from &myVertexVector[0] to the GPU, you’ll have a perfect array of positions!
  • Right now, your array holds nothing but vertex positions. Modify your pVertexLayout to match, or add the COLOR and SOME_MORE_DATA attributes from the previous example into your MyVertex and give them reasonable values.
  • And in your Render() loop, make sure that m_pDeviceContext->Draw(...) is told the correct amount of vertices to draw.
  • Don’t forget to change your vertex shader if you make any changes to the vertex layout!

Finally, if you don’t use the shitty-bell FBX attached up at the top of this post, take note. In its default state, DirectX will only draw polygons with an [X,Y] from [-1,-1] to [1,1] and with a Z from 0 to 1. Look through the vertices in your FBX and make sure everything is in that range!

And by the end of it all, here’s what you’ll see:

Wow, this image of a janky purple bell was worth all the hours I have spent following your tutorials, Ben!

Wow, Ben, this image of a janky purple bell was worth all the hours I have spent following your tutorials!