I'm generating light maps for scene mesh objects using DirectX's UV Atlas Tool( D3DXUVAtlasCreate() ). I've succeeded in generating an atlas, however, when I try to render the mesh object using the atlas the seams are visible on the mesh. Below are images of a lightmap generated for a cube. Here is the code I use to generate a uv atlas for a cube:

struct sVertexPosNormTex
    D3DXVECTOR3 vPos, vNorm;
    sVertexPosNormTex( D3DXVECTOR3 v, D3DXVECTOR3 n, D3DXVECTOR2 uv )
        vPos = v;
        vNorm = n;
        vUV = uv;

// create a light map texture to fill programatically
    hr = D3DXCreateTexture( pd3dDevice, 128, 128, 1, 0, D3DFMT_A8R8G8B8,
        D3DPOOL_MANAGED, &pLightmap );
    if( FAILED( hr ) )
        DebugStringDX( "Main", "Failed to D3DXCreateTexture( lightmap )", __LINE__, hr );
        return hr;

    // get the zero level surface from the texture
    IDirect3DSurface9 *pS = NULL;
    pLightmap->GetSurfaceLevel( 0, &pS );

    // clear surface
    pd3dDevice->ColorFill( pS, NULL, D3DCOLOR_XRGB( 0, 0, 0 ) );

    // load a sample mesh
    DWORD dwcMaterials = 0;
    LPD3DXBUFFER pMaterialBuffer = NULL;
    V_RETURN( D3DXLoadMeshFromX( L"cube3.x", D3DXMESH_MANAGED, pd3dDevice, &pAdjacency, &pMaterialBuffer, NULL, &dwcMaterials, &g_pMesh ) );

    // generate adjacency
    DWORD *pdwAdjacency = new DWORD[ 3 * g_pMesh->GetNumFaces() ];
    g_pMesh->GenerateAdjacency( 1e-6f, pdwAdjacency );

    // create light map coordinates
    LPD3DXMESH pMesh = NULL;
    LPD3DXBUFFER pFacePartitioning = NULL, pVertexRemapArray = NULL;
    FLOAT resultStretch = 0;
    UINT numCharts = 0;

    hr = D3DXUVAtlasCreate( g_pMesh, 0, 0, 128, 128, 3.5f, 0, pdwAdjacency, NULL, NULL, NULL, NULL, NULL, 0, &pMesh, 
        &pFacePartitioning, &pVertexRemapArray, &resultStretch, &numCharts );
    if( SUCCEEDED( hr ) )
        // release and set mesh
        SAFE_RELEASE( g_pMesh );
        g_pMesh = pMesh;

        // write mesh to file
        hr = D3DXSaveMeshToX( L"cube4.x", 
            ( const D3DXMATERIAL* )pMaterialBuffer->GetBufferPointer(), 
            D3DXF_FILEFORMAT_TEXT );
        if( FAILED( hr ) )
            DebugStringDX( "Main", "Failed to D3DXSaveMeshToX() at OnD3D9CreateDevice()", __LINE__, hr );

        // fill the the light map
        hr = BuildLightmap( pS, g_pMesh );
        if( FAILED( hr ) )
            DebugStringDX( "Main", "Failed to BuildLightmap()", __LINE__, hr );
        DebugStringDX( "Main", "Failed to D3DXUVAtlasCreate() at OnD3D9CreateDevice()", __LINE__, hr ); 

    SAFE_DELETE_ARRAY( pdwAdjacency );
    SAFE_RELEASE( pFacePartitioning );
    SAFE_RELEASE( pVertexRemapArray );
    SAFE_RELEASE( pMaterialBuffer );

Here is code to fill lightmap texture:

HRESULT BuildLightmap( IDirect3DSurface9 *pS, LPD3DXMESH pMesh )
    HRESULT hr = S_OK;

    // validate lightmap texture surface and mesh
    if( !pS 
        || !pMesh )
        return E_POINTER;

    // lock the mesh vertex buffer
    sVertexPosNormTex *pV = NULL;
    pMesh->LockVertexBuffer( D3DLOCK_READONLY, ( void** )&pV );

    // lock the mesh index buffer
    WORD *pI = NULL;
    pMesh->LockIndexBuffer( D3DLOCK_READONLY, ( void** )&pI );

    // get the lightmap texture surface description
    pS->GetDesc( &desc );

    // lock the surface rect to fill with color data
    hr = pS->LockRect( &rct, NULL, 0 );
    if( FAILED( hr ) )
        DebugStringDX( "main.cpp:", "Failed to IDirect3DTexture9::LockRect()", __LINE__, hr );
        return hr;

    // iterate the pixels of the lightmap texture
    //      check each pixel to see if it lies between the uv coordinates of a cube face
    BYTE *pBuffer = ( BYTE* )rct.pBits;
    for( UINT y = 0; y < desc.Height; ++y )
        BYTE* pBufferRow = ( BYTE* )pBuffer;
        for( UINT x = 0; x < desc.Width * 4; x+=4 )
            // determine the pixel's uv coordinate
            D3DXVECTOR2 p( ( ( float )x / 4.0f ) / ( float )desc.Width + 0.5f / 128.0f, y / ( float )desc.Height + 0.5f / 128.0f );

            // for each face of the mesh
            //      check to see if the pixel lies within the face's uv coordinates
            for( UINT i = 0; i < 3 * pMesh->GetNumFaces(); i +=3 )
                sVertexPosNormTex v[ 3 ];
                v[ 0 ] = pV[ pI[ i + 0 ] ];
                v[ 1 ] = pV[ pI[ i + 1 ] ];
                v[ 2 ] = pV[ pI[ i + 2 ] ];

                if( TexcoordIsWithinBounds( v[ 0 ].vUV, v[ 1 ].vUV, v[ 2 ].vUV, p ) )
                    // the pixel lies b/t the uv coordinates of a cube face
                    // light contribution functions aren't needed yet
                    //D3DXVECTOR3 vPos = TexcoordToPos( v[ 0 ].vPos, v[ 1 ].vPos, v[ 2 ].vPos, v[ 0 ].vUV, v[ 1 ].vUV, v[ 2 ].vUV, p );
                    //D3DXVECTOR3 vNormal = v[ 0 ].vNorm;

                    // set the color of this pixel red( for demo )
                    BYTE ba[] = { 0, 0, 255, 255, };

                    //ComputeContribution( vPos, vNormal, g_sLight, ba );

                    // copy the byte array into the light map texture
                    memcpy( ( void* )&pBufferRow[ x ], ( void* )ba, 4 * sizeof( BYTE ) );

        // go to next line of the texture
        pBuffer += rct.Pitch;

    // unlock the surface rect

    // unlock mesh vertex and index buffers

    // write the surface to file
    hr = D3DXSaveSurfaceToFile( L"LightMap.jpg", D3DXIFF_JPG, pS, NULL, NULL );
    if( FAILED( hr ) )
        DebugStringDX( "Main.cpp", "Failed to D3DXSaveSurfaceToFile()", __LINE__, hr );

    return hr;
bool TexcoordIsWithinBounds( const D3DXVECTOR2 &t0, const D3DXVECTOR2 &t1, const D3DXVECTOR2 &t2,
                     const D3DXVECTOR2 &p )
    // compute vectors
    D3DXVECTOR2 v0 = t1 - t0, 
        v1 = t2 - t0,
        v2 = p - t0;

    float f00 = D3DXVec2Dot( &v0, &v0 );
    float f01 = D3DXVec2Dot( &v0, &v1 );
    float f02 = D3DXVec2Dot( &v0, &v2 );
    float f11 = D3DXVec2Dot( &v1, &v1 );
    float f12 = D3DXVec2Dot( &v1, &v2 );

    // Compute barycentric coordinates
    float invDenom = 1 / ( f00 * f11 - f01 * f01 );
    float fU = ( f11 * f02 - f01 * f12 ) * invDenom;
    float fV = ( f00 * f12 - f01 * f02 ) * invDenom;

    // Check if point is in triangle
    if( ( fU >= 0 ) && ( fV >= 0 ) && ( fU + fV < 1 ) )
        return true;

    return false;



I believe the problem comes from the difference between the lightmap uv coordinates and the pixel center coordinates...for example, here are the lightmap uv coordinates( generated by D3DXUVAtlasCreate() ) for a specific face( tri ) within the mesh, keep in mind that I'm using the mesh uv coordinates to write the pixels for the texture:

v[ 0 ].uv = D3DXVECTOR2( 0.003581, 0.295631 );

v[ 1 ].uv = D3DXVECTOR2( 0.003581, 0.003581 );

v[ 2 ].uv = D3DXVECTOR2( 0.295631, 0.003581 );

the lightmap texture size is 128 x 128 pixels. The upper-left pixel center coordinates are: float halfPixel = 0.5 / 128 = 0.00390625; D3DXVECTOR2 pixelCenter = D3DXVECTOR2( halfPixel, halfPixel );

will the mapping and sampling of the lightmap texture will require that an offset be taken into account or that the uv coordinates are snapped to the pixel centers..?

...Any ideas on the best way to approach this situation would be appreciated...What are the common practices?

  • \$\begingroup\$ You are most likely correct in your assumption that it is related to imprecise coordinates. What's actually happening is bilinear interpolation - when the texture coordinates don't match the texels exactly, it will take some of the color from the nearest pixel (along the x and y axes, hence the "bi" in the name). The solution below (expanding the edges of the images on the lightmap) will most likely work because of this. Another solution (or a way to test this) would be to set the texture filtering mode to nearest, which "snaps" the imprecise coordinates to the texels. \$\endgroup\$ – maul Aug 14 '13 at 20:57
  • \$\begingroup\$ @maul thanks for the suggestion! I tried changing the texture filtering to "Point"( nearest for DirectX ). It fixed some areas but still left areas "black"...the solution below indeed was effective...although both solutions( together ) could effectively be utilized... \$\endgroup\$ – P. Avery Aug 14 '13 at 22:58

In the past I've fixed this sort of problem using a dilation filter. Basically, the idea is to go over the lightmap after it's rendered and expand the borders of all the pieces by a few pixels, by copying the values of filled pixels into adjacent empty pixels. This can be done with a pixel shader in a full-screen pass or two over the lightmap.

  • \$\begingroup\$ @P.Avery Right, you have to be careful to avoid overwriting pixels from neighboring faces. It's useful for faces to be set a few pixels apart in the lightmap, if possible, to give some space for the dilation filter to work. (Unless the faces are actually adjacent on the model, in which case they can be adjacent in the lightmap as well with no border, since the edges will match.) \$\endgroup\$ – Nathan Reed Aug 14 '13 at 21:16
  • \$\begingroup\$ Thanks!...I tried something like this already( within software ) by using the corner points of each pixel for bounds determination( TexcoordIsWithinBounds() ). This would include any pixel with at least one corner point overlapping a face. This method fixed a lot of the pixels. I ultimately decided not to use it to avoid overwriting the pixels of neighboring faces on the lightmap...you're suggestion then is to sample the surrounding pixels for a given pixel, and, if that pixel has not yet been written to, copy the color from the neighboring pixel to the input pixel..?? \$\endgroup\$ – P. Avery Aug 14 '13 at 22:06
  • \$\begingroup\$ I implemented dilation filtering using a quad the size of the lightmap texture...it worked perfectly right away. \$\endgroup\$ – P. Avery Aug 14 '13 at 23:02

The dilatation will seem to work, but it is an imperfect solution because it is undersampled, it re-uses information (entropy coming from light irradiance samples) and spread it over, effectively creating extrapolation.
The correct thing to do here is to use conservative rasterization which is not trivial at all to do. Your rasterization is based on the very classic barycentric coordinates is-pt-in-triangle test. This is not conservative and thus some texels which HAVE partial surface intersected in the triangle don't get chosen because the test point (center?) is over the mathematical edge (infinitely thin).
There are some papers about conservative rasterization, but they all implement fairly complex methods. You're free to do your own hack here. e.g. like multisampling the point in the texel (lumel) test, that you test for inclusion so that you have greater chance of picking texels on the edge. that would be the fastest way to somehow-conservativeness :)

Or you can just be happy with dilatation and call it a day. Anyway, the conservative rasterization is not enough in itself because of filtering issues later. And obviously as well, never try to generate mipmaps for lightmaps, directx will do it automatically in numerous cases therefore dont forget to force it off (only 1 mip). Otherwise you are going to see the black re-appear for afar objects.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.