Cubemaps

The other day I wanted to create a renderpass for realtime previewing of my cubemaps. I wanted to go for the lazy way of just googling for some code, and copy-pasting the proper coordinates into my code. However, I could not actually find such code at all. There seems to be remarkably little in-depth information around on cubemaps, let alone code for non-standard uses of these cubemaps. I had to do it the old-fashioned way: draw it out on paper, then build the geometry by hand. So, I figured I might as well do a quick blog on them, so that there will be a place to find that information in the future.

First, a brief overview of cubemaps. They are a very useful type of environment map, with which you can perform things like image-based lighting, reflection, and many other tricks. I suppose most graphics programmers are already familiar with them anyway. However, I have found over the years that their understanding is generally quite superficial. They may know how to use them, but things like how the texture coordinates are actually calculated remain a mystery. So let’s start at the beginning.

Cubemap addressing

A cubemap is a collection of 6 square 2D textures, each mapped to a face of a cube (hence the name). It can be visualized like this:

cube_map2

For a texture lookup, a 3D vector is used as a texture coordinate. Think of this vector as a ray from the center of the cube, going through one of the faces. The texture is sampled at the intersection of the ray with the face. Or more intuitively: the viewer is at the center of the cube, looking in a certain direction. The vector is that direction.

Cubemaps are often previewed in 2D in a cross arrangement, like this:

cubemap

Here you can clearly see how the 6 faces encode an entire environment, for the full 360 degrees. On MSDN there is a nice overview of how the texture coordinates are mapped out:

As you can see, the cube is axis-aligned, so the faces correspond with the X, Y and Z axis, where each axis has a negative and a positive face in the cubemap (-x, +x, -y, +y, -z and +z respectively).

It does not explain how the texture coordinate are actually calculated however. You can find that on an old nVidia page covering cubemaps though (although it is OpenGL-oriented, so it uses (S,T) rather than (U,V) as texture coordinates). The texture mapping works in two stages:

1) The face is selected by looking at the absolute values of the components of the 3d vector (|x|, |y|, |z|). The component with the absolute value of the largest magnitude determines the major axis. The sign of the component selects the positive or negative direction.

2) The selected face is addressed as a regular 2D texture with U, V coordinates within a (0..1) range. The U and V are calculated from the two components that were not the major axis. So for example, if  we have +x as our cubemap face, then Y and Z will be used to calculate U and V:

U = ((-Z/|X|) + 1)/2

V = ((-Y/|X|) + 1)/2

Since X had the largest magnitude, dividing Y and Z by |X| will bring them within a (-1..1) range. This is then translated to a (0..2) range, and finally scaled to (0..1) range. Now we can do a regular 2D texture lookup. You can work out the formulas for the other 5 faces by looking at the mappings above. You can see which axis maps to U and V in each case, and you can see the direction in which the axis goes, compared to U/V, to see whether you need to flip the sign or not.

Note that since the texture coordinates are scaled by the major axis, it is not required to normalize the 3D vector. Regardless of the length of the vector, the coordinates will always end up in (0..1) range. Normalizing has no effect, it is just another scaling operation, which is made redundant by the texture coordinate calculation. In fact, on early hardware, cubemaps were often used to normalize vectors for per-pixel operations (the cubemap would just contain a normalized vector encoding the lookup vector for each texel). This early hardware would either not be able to perform a normalization per-pixel at all, or a cubemap lookup may have been cheaper than an arithmetic solution.

Note also that the texture coordinates have to be calculated on a per-pixel basis. If you were to do this per-vertex, you might have the problem that not all three vertices will look at the same face, and therefore not all vertices will address the same 2D texture. The face can change at any point inside a triangle.

Dynamic cubemaps

There are other types of environment maps than cubemaps. A popular one is the spherical map:

envmap

This type of environment map was very popular in the early days, because it is very easy to calculate texture coordinates for it, without requiring special hardware support (and also quite efficient in software, very popular for faking phong shading in the early 90s). Namely, if you take a normalized vector (x,y,z) as your view direction, you can derive the texture coordinates like this:

U = (x + 1) / 2

V = (y + 1) / 2

However, the cubemap has some advantages over other environment mappings. As you can already see in this spherical image, the resolution is very poor towards the edges. A cubemap has a very uniform mapping of pixels in all directions.

Another advantage is that cubemaps can very easily be updated in realtime with render-to-texture. You can render an environment map by just doing a renderpass for each cube face.  Each face is square, and there are 4 faces going round horizontally in 360 degrees (front, right, back, left), so each face covers a 90 degree viewing angle (and then the extra 2 faces going round vertically, top and bottom). All you have to do is set up a camera positioned where you want the center of your environment map to be, facing in the right direction, with field-of-view of 90 degrees horizontally and vertically, and render to the respective face.

For dynamically updating other types of maps, such as spherical maps, you would first need to render to a set of 2D textures, and then you would need an extra pass with a special mesh to compose them into a spherical map with the correct warping.

Previewing a cubemap in realtime

I wanted to create a 2D preview of the contents of a cubemap, in the usual cross arrangement. The first idea then would be to just create 6 quads for the cross geometry, with 2D textures on them. So, a cubemap consists of 2D textures. Or does it? Well, conceptually it does. But not all 3D APIs actually let you manipulate the individual faces as textures. That is the problem I ran into. In Direct3D 10+, there is no specific cubemap datatype. There is a generic texture array datatype, and if you create an array of 6 2D textures, it can be used as a cubemap. But it’s still an array of 2D textures, so you can still use the faces directly as regular 2D textures (or rendertargets).

In Direct3D 9 however, a cubemap is a specific type of texture. You can access the individual faces as surfaces, but not as textures. You could create a set of 2D textures of the same dimensions, and then copy each cubemap surface to a texture, but that will clearly have some extra overhead.

So instead I wanted to map the cubemap onto the cross directly. For that, I had to derive the proper 3D vectors at each vertex of the cross. Since we already know that they do not have to be normalized, it can be done within the range of (-1..1) for each vector component, which is more intuitive than having to deal with normalized vectors.

So, I assigned indices to each vertex in the layout, and worked out which vertices fit to where, and then derived the components of the view vector. The indices are in orange, the view vectors in blue:

Cubemap

Every view vector is shared by 3 faces. So we need to have the following sets of vertices that share the same vector:

  • 0, 2, 6
  • 1,5
  • 7, 11, 12
  • 10, 13

The rest of the vertices are interior to the cross, and sharing is done automatically.

Normally my vertex formats would only have 2D texture coordinates. If I were to store the view vector as texture coordinates, I would need to define a new vertex format. However, since the per-vertex normal has no meaning for this preview mesh, I decided to just encode the view vector in the normalvector of each vertex instead. This way I would not need any custom vertex format for a cubemap preview. I would just need a pixel shader that sampled the cubemap with the normal vector.

For the positions, I decided to put the cross inside a unit square. The width has a (-0.5..0.5) range. The height has a (-0.375..0.375) range, since it is 4 faces wide but only 3 faces high. This gives me the following vertices (this is the list I was hoping to find and just copy-paste into my own code):

vertexBuffer[] = {
	{ -0.25f, 0.375f, 0.0f,
	   -1,  1, -1 },
	{   0.0f, 0.375f, 0.0f,
	    1,  1, -1 },
	{  -0.5f, 0.125f, 0.0f,
	   -1,  1, -1 },
	{ -0.25f, 0.125f, 0.0f,
	   -1,  1,  1 },
	{   0.0f, 0.125f, 0.0f,
	    1,  1,  1 },
	{  0.25f, 0.125f, 0.0f,
	    1,  1, -1 },
	{   0.5f, 0.125f, 0.0f,
	   -1,  1, -1 },
	{  -0.5f, -0.125f, 0.0f,
	   -1, -1, -1 },
	{ -0.25f, -0.125f, 0.0f,
	   -1, -1,  1 },
	{   0.0f, -0.125f, 0.0f,
	    1, -1,  1 },
	{  0.25f, -0.125f, 0.0f,
	   1, -1, -1 },
	{   0.5f, -0.125f, 0.0f,
	   -1, -1, -1 },
	{ -0.25f, -0.375f, 0.0f,
	   -1, -1, -1 },
	{   0.0f, -0.375f, 0.0f,
	    1, -1, -1 }
};

And with that mesh, I can finally preview my cubemaps directly. It is an elegant solution as well, compared to having to use 6 separate textures (which means you need to either abuse multitexture heavily, or render each plane with a separate call, because you have to switch textures). And it works for all 3D APIs, since you are actually using the cubemap itself, and there is no need for a workaround copying the texture to a separate 2D texure.

This entry was posted in Direct3D, OpenGL, Software development and tagged , , , , , , , , , . Bookmark the permalink.

8 Responses to Cubemaps

  1. Ravi says:

    Can you please explain this sentence “You can see which axis maps to U and V in each case, and you can see the direction in which the axis goes, compared to U/V, to see whether you need to flip the sign or not.”

    • Scali says:

      You see the U and V axis in the image, right? U goes from left to right, V goes from top to bottom. If you look at the axes in each face, sometimes you get +X, +Y or +Z going from left to right, or from top to bottom, and sometimes it is -X, -Y or -Z (so the axis goes the other direction). So for example, for face 5 (-Z), you need to map -X to U, so you need to flip the sign.

      • Ravi says:

        Thank you :). Now it makes sense. I figured out how the face selections are done (if a ray begins at origin and it intersects face 0 or face 1 its X component must be greater than (or equal to)? Y and Z components ) but I could not understand how the axes are chosen for each face. Please tell me how the axis for each map is figured out like y and
        -z for Face 0?

      • Scali says:

        The axes are just ‘hardwired’ that way. Just like the faces are ‘hardcoded’ to certain order (ie. face 0 is +X, face 1 is -X etc). It’s just how cubemaps were defined in D3D7, and OpenGL works the same way. So for each of the 6 faces it is also defined how the X, Y and Z map to U and V.
        So you just look at the image above, and figure out from the drawing how each is mapped.
        E.g., if you look at face 0, then +X is the major axis, so Y and Z have to be mapped to U and V.
        We see that -Z goes from left to right, so that maps to U.
        We see that Y goes from bottom to top, so we need to map -Y to V, to get from top to bottom.

        For face 1, you have -X as the major axis.
        Now we have +Z going from left to right, so we map that to U.
        And again Y goes from bottom to top, so again we map -Y to V.

        The same goes for the other 4 axes, you can just derive them from the drawing.

        Or you can look at the table here: http://www.nvidia.com/object/cube_map_ogl_tutorial.html
        See under “Mapping Texture Coordinates to Cube Map Faces”, they already worked it out for all faces.

  2. Hi Scali. I can’t believe it but you blog post is actually the only resource I could find that is explaining how to map TextureCube UV to ‘cross’ geometry. It would be great if you could provide c++ and shader code as a reference for the example above.

    I’m still having the problems though. I have a feeling that one of the UV components is fixed during shader sampling. I know it’s a lot to ask but could you please take a look in to my code snippet.

    Creation of the vertex buffer http://pastie.org/9695490
    Shader code http://pastie.org/9695497
    That’s how it should look like http://i62.tinypic.com/2cxdfr8.jpg
    That’s how it does look like http://i57.tinypic.com/11qgw44.jpg

    Any help or suggestion will be much appreciated. Thank you!

    • Scali says:

      I may be able to help you later… In the meantime… Shortly after I wrote this blog, I found that Humus also has a Cubemap viewer tool, which comes with source code (including a version of the geometry that I worked out manually above): http://www.humus.name/index.php?page=Textures
      Perhaps it’s of some use to you.

      Edit: a quick perusal of your code shows me that you’re using 2d tex coordinates (DXGI_FORMAT_R32G32_FLOAT). So D3D is probably just not interpreting your data correctly.

  3. Pingback: Your home for high-quality video (now including 360) - Vimeo Blog

  4. Pingback: Unity Toy Path Tracer – The Instruction Limit

Leave a comment