2011-10-28

Knight's Leap ($0.99, iOS)

I co-developed a puzzle game for iOS and it was released on 27 October on the App Store ($0.99). In the game you ride your knight on the chessboard, filling empty squares as fast as you can or creating a path.

2011-09-05

Shadow Volumes and non-closed meshes

Today I implemented an algorithm that produces correct shadow volumes for non-closed meshes, aka meshes with open edges. I couldn't find it anywhere on the Internet so I decided to publish it here. It's from Game Programming Gems 4. The gist of the algorithm is to add inverted face normals and point edges with missing neighboring face to them. To use it, call generateOpenEdgeTriangles() after generateEdges().

#define NO_NEIGHBOR 65535


// Indices of two vertices that belong to the same triangle. Used to construct shadow volume sides.
struct TriangleEdge
{
    TriangleEdge() : v1(0), v2(0), f1(NO_NEIGHBOR), f2(NO_NEIGHBOR) {}
    TriangleEdge( unsigned short a_v1, unsigned short a_v2 ) : v1(a_v1), v2(a_v2) {}

    unsigned short v1;
    unsigned short v2;
    unsigned short f1;
    unsigned short f2;
};






bool ShadowVolume::doesEdgeBelongToFace( TriangleEdge& edge, int aFaceIndex ) const
{
    // A and B.
    if ((almostEquals( meshVertices[ meshFaces[ aFaceIndex ].x ], meshVertices[ edge.v1 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].y ], meshVertices[ edge.v2 ] )) ||
        (almostEquals( meshVertices[ meshFaces[ aFaceIndex ].x ], meshVertices[ edge.v2 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].y ], meshVertices[ edge.v1 ] )))
    {
        return true;
    }
    
    // B and C.
    if ((almostEquals( meshVertices[ meshFaces[ aFaceIndex ].y ], meshVertices[ edge.v1 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].z ], meshVertices[ edge.v2 ] )) ||
        (almostEquals( meshVertices[ meshFaces[ aFaceIndex ].y ], meshVertices[ edge.v2 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].z ], meshVertices[ edge.v1 ] )))
    {
        return true;
    }
    
    // C and A.
    if ((almostEquals( meshVertices[ meshFaces[ aFaceIndex ].z ], meshVertices[ edge.v1 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].x ], meshVertices[ edge.v2 ] )) ||
        (almostEquals( meshVertices[ meshFaces[ aFaceIndex ].z ], meshVertices[ edge.v2 ] ) &&
         almostEquals( meshVertices[ meshFaces[ aFaceIndex ].x ], meshVertices[ edge.v1 ] )))
    {
        return true;
    }
    return false;
}



void ShadowVolume::generateEdges()
{
    TriangleEdge edge;
    
    for (unsigned int f = 0; f < meshFaces.getSize(); ++f)
    {        
        edge.v1 = meshFaces[ f ].x;
        edge.v2 = meshFaces[ f ].y;
        edge.f1 = f;
        edge.f2 = NO_NEIGHBOR;
        
        // Finds the other face that's touching vertices v1 and v2.
        for (unsigned int f2 = 0; f2 < meshFaces.getSize(); ++f2)
        {
            if (f2 == f)
            {
                continue;
            }
            if (doesEdgeBelongToFace( edge, f2 ))
            {
                edge.f2 = f2;
                break;
            }
        }
        edges[ f * 3 + 0 ] = edge;
        
        edge.v1 = meshFaces[ f ].y;
        edge.v2 = meshFaces[ f ].z;
        edge.f1 = f;
        edge.f2 = NO_NEIGHBOR;
        
        // Finds the other face that's touching vertices v1 and v2.
        for (unsigned int f2 = 0; f2 < meshFaces.getSize(); ++f2)
        {
            if (f2 == f)
            {
                continue;
            }
            if (doesEdgeBelongToFace( edge, f2 ))
            {
                edge.f2 = f2;
                break;
            }
        }
        edges[ f * 3 + 1 ] = edge;
        
        edge.v1 = meshFaces[ f ].z;
        edge.v2 = meshFaces[ f ].x;
        edge.f1 = f;
        edge.f2 = NO_NEIGHBOR;
        
        // Finds the other face that's touching vertices v1 and v2.
        for (unsigned int f2 = 0; f2 < meshFaces.getSize(); ++f2)
        {
            if (f2 == f)
            {
                continue;
            }
            if (doesEdgeBelongToFace( edge, f2 ))
            {
                edge.f2 = f2;
                break;
            }
        }
        edges[ f * 3 + 2 ] = edge;
    }
}


void ShadowVolume::generateOpenEdgeTriangles()
{
    int degenFacesSize = 0;


    // Finds out the # of open edges.
    for (unsigned int e = 0; e < edges.getSize(); ++e)
    {
        if (edges[ e ].f2 == NO_NEIGHBOR)
        {
            ++degenFacesSize;
        }
    }
    int offs = meshFaces.getSize();
    meshFaceNormals.resize( offs + degenFacesSize );
    backFaces.resize( offs + degenFacesSize );    


    for (unsigned int e = 0; e < edges.getSize(); ++e)
    {
        if (edges[ e ].f2 == NO_NEIGHBOR)
        {
            meshFaceNormals[ offs ] = -meshFaceNormals[ edges[e].f1 ];


            edges[ e ].f2 = offs;


            ++offs;
        }
    }
}

2011-04-03

New Release of Aether3D

I uploaded release 3. It mostly contains bug fixes (Debug build now works in Visual Studio etc.), but new features include support of PVR compressed textures and automatic loading of compressed textures if present. Download the release here.

In other news, Death Rally for iOS is selling well and the first update is coming soon. My role in the development has mostly been graphics performance profiling and optimizing and other graphics related stuff.

2011-02-23

Random iPhone Developer Tips

1. Adding Default@2x.png to an SVN repository using command line client.
Command line svn client doesn't like the "@"-character. Oddly enough, escaping it with "\" doesn't work, but what works is this: "svn add Default@2x.png@"

2. Resizing your website to fit the iPhone screen.
Just add this tag to your head-section (replace square brackets with angle brackets): [meta name="viewport" id="iphone-viewport" content="width=480,maximum-scale=0.66667"]