Jump to content
  • Advertisement
Sign in to follow this  
utilae

Rectangle Packing Algorithm bug - need help

This topic is 4202 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hi, I am using C++ and direct3d9. In this code I only use vectors. I have recently been modifying my texture/rectangle packing function, but I am having a problem, a crash which only occurs if I use a variation of my compareLoadedTex() sort function (see second quoted code section). The variation sorts by area, but I have checked all the variables that it sorts by and they are good. I noticed further that if I comment out the erase() function the crash never occurs, though that makes the rectangle packing function not work. Anyone able to help? Thanks
//in .cpp file

//Combine all textures in MergeList into one texture
void CTextureManager::MergeTextures(const string &sMergedTextureName,const bool &bClearTextures)
{
	//sort m_lstMergeList, largest area first, smallest last
	sort(m_lstMergeList.begin(),m_lstMergeList.end(),compareLoadedTex());

	vector <RECT> lstFilledRects;//list of inserted textures to make up final merged texture
	vector <RECT> lstHoles;//holes in merged texture that have to be filled with textures

	//use total area of all textures to be merged, to try and make the final texture as square as possible
	double dTextureWidth=sqrt(m_dTotalArea);
	if(dTextureWidth<32)
		dTextureWidth=32;
	else if(dTextureWidth>32 && dTextureWidth<64)
		dTextureWidth=64;
	else if(dTextureWidth>64 && dTextureWidth<128)
		dTextureWidth=128;
	else if(dTextureWidth>128 && dTextureWidth<256)
		dTextureWidth=256;
	else if(dTextureWidth>256 && dTextureWidth<512)
		dTextureWidth=512;
	else if(dTextureWidth>512 && dTextureWidth<1024)
		dTextureWidth=1024;
	else if(dTextureWidth>1024)
		dTextureWidth=2048;
	//could put in more for bigger textures

	//the first hole is the container, the final size of the merged texture
	RECT recContainer;
	recContainer.top=0;
	recContainer.left=0;
	recContainer.right=dTextureWidth;
	recContainer.bottom=dTextureWidth;
	lstHoles.push_back(recContainer);

	//put every texture to merge in a hole until there are no more holes or all textures have been placed
	int nHoleIndex=0;
	vector<LOADEDTEXTURE>::iterator itTexture=m_lstMergeList.begin();
	while(itTexture!=m_lstMergeList.end() && lstHoles.empty()==false)
	{
		//go back to first hole if there are still textures to put in holes
		if(nHoleIndex>lstHoles.size())
			nHoleIndex=0;

		//determine bin width and height
		int nHoleHeight=lstHoles[nHoleIndex].bottom-lstHoles[nHoleIndex].top;
		int nHoleWidth=lstHoles[nHoleIndex].right-lstHoles[nHoleIndex].left;

		//case (e) or (f): texture too tall for bin or too wide for bin
		if(itTexture->m_nHeight > nHoleHeight ||
			itTexture->m_nWidth > nHoleWidth)
			++nHoleIndex;//cannot fit in bin, try next hole
		else //can fit
		{
			//will fit in bin so put texture in top left
			RECT newFilledRect;
			newFilledRect.top=lstHoles[nHoleIndex].top;
			newFilledRect.left=lstHoles[nHoleIndex].left;
			newFilledRect.right=lstHoles[nHoleIndex].left+itTexture->m_nWidth;
			newFilledRect.bottom=lstHoles[nHoleIndex].top+itTexture->m_nHeight;
			lstFilledRects.push_back(newFilledRect);	

			//update lowest bound of texture usage
			if(newFilledRect.bottom>recContainer.bottom)
				recContainer.bottom=newFilledRect.bottom;

			//add replacement holes if there are any	
			RECT newHole;
			//case (a): Perfect width. Too short.
			if(itTexture->m_nWidth==nHoleWidth)
			{
				//case (a): Perfect width. Too short.
				if(itTexture->m_nHeight<nHoleHeight)
				{
					//one hole below
					newHole.top=lstHoles[nHoleIndex].top+itTexture->m_nHeight;
					newHole.left=lstHoles[nHoleIndex].left;
					newHole.right=lstHoles[nHoleIndex].right;
					newHole.bottom=lstHoles[nHoleIndex].bottom;
					lstHoles.push_back(newHole);
				}
				//case (d): Perfect width and height.
				else if(itTexture->m_nHeight==nHoleHeight)
				{
					//no holes
				}
			}
			else if(itTexture->m_nWidth<nHoleWidth)
			{
				//case (b): Too narrow. Too short.
				if(itTexture->m_nHeight<nHoleHeight)
				{
					//one hole below
					newHole.top=lstHoles[nHoleIndex].top+itTexture->m_nHeight;
					newHole.left=lstHoles[nHoleIndex].left;
					newHole.right=lstHoles[nHoleIndex].left+itTexture->m_nWidth;
					newHole.bottom=lstHoles[nHoleIndex].bottom;
					lstHoles.push_back(newHole);

					//one hole to right
					newHole.top=lstHoles[nHoleIndex].top;
					newHole.left=lstHoles[nHoleIndex].left+itTexture->m_nWidth;
					newHole.right=lstHoles[nHoleIndex].right;
					newHole.bottom=lstHoles[nHoleIndex].bottom;
					lstHoles.push_back(newHole);
				}
				//case (c): Too narrow. Perfect height.
				else if(itTexture->m_nHeight==nHoleHeight)
				{
					//one hole to right
					newHole.top=lstHoles[nHoleIndex].top;
					newHole.left=lstHoles[nHoleIndex].left+itTexture->m_nWidth;
					newHole.right=lstHoles[nHoleIndex].right;
					newHole.bottom=lstHoles[nHoleIndex].bottom;
					lstHoles.push_back(newHole);
				}
			}
			//remove old hole
			lstHoles.erase(lstHoles.begin()+nHoleIndex);//crashes here, unless this line comented out, unless the compareLoadedTex() sort function does not sort by area - see section of code quoted in the next section

			//sort list of holes
			sort(lstHoles.begin(),lstHoles.end(),compareHoleRects());

			++itTexture;
		}
	}

	//create texture the size of 'container'
	LOADEDTEXTURE newTexture;
	HRESULT hrCreateTex=D3DXCreateTexture(g_pD3DDevice9,recContainer.right,recContainer.bottom,D3DX_DEFAULT,0,D3DFMT_A8R8G8B8,D3DPOOL_MANAGED,&newTexture.m_texture);
	g_Log<<"MergeTextures("<<sMergedTextureName<<") Create Texture: "<<DXGetErrorString9(hrCreateTex)<<endl;
	
	//setup surfaces for copying textures
	LPDIRECT3DSURFACE9 pSrcSurface=NULL;
	LPDIRECT3DSURFACE9 pDestSurface=NULL;
	newTexture.m_texture->GetSurfaceLevel(0,&pDestSurface);
	
	//get correct width and height of texture (incase texture size is adjusted automatically when created)
	D3DSURFACE_DESC surfaceDesc;
	newTexture.m_texture->GetLevelDesc(0,&surfaceDesc);
	recContainer.right=surfaceDesc.Width;
	recContainer.bottom=surfaceDesc.Height;

	//store textures filename, height and width
	newTexture.m_sName=sMergedTextureName;
	newTexture.m_nHeight=recContainer.bottom;
	newTexture.m_nWidth=recContainer.right;
	newTexture.m_nArea=recContainer.bottom*recContainer.right;

	//go through list of textures and filled rects and copy textures to container texture
	sort(m_lstMainTextureList.begin(),m_lstMainTextureList.end(),SortByNameAscending<LOADEDTEXTURE>());

	vector<RECT>::iterator itCurrentRECT=lstFilledRects.begin();
	vector<TILE>::iterator itTiles;
	while(itCurrentRECT!=lstFilledRects.end())
	{
		//copy from texture to container texture
		m_lstMergeList[0].m_texture->GetSurfaceLevel(0,&pSrcSurface);
		D3DXLoadSurfaceFromSurface(pDestSurface,NULL,&(*itCurrentRECT),pSrcSurface,NULL,NULL,D3DX_FILTER_NONE,0);
		pSrcSurface->Release();

		//add tiles to merged texture, from each texture
		itTiles=m_lstMergeList[0].m_TileList.begin();
		while(itTiles!=m_lstMergeList[0].m_TileList.end())
		{
			//recalculate tile coordinates
			TILE newTile;
			newTile.m_sName=itTiles->m_sName;
			newTile.m_tvTop=(float)((itTiles->m_tvTop*m_lstMergeList[0].m_nHeight)+itCurrentRECT->top)/recContainer.bottom;
			newTile.m_tuLeft=(float)((itTiles->m_tuLeft*m_lstMergeList[0].m_nWidth)+itCurrentRECT->left)/recContainer.right;
			newTile.m_tuRight=(float)((itTiles->m_tuRight*m_lstMergeList[0].m_nWidth)+itCurrentRECT->left)/recContainer.right;
			newTile.m_TvBottom=(float)((itTiles->m_TvBottom*m_lstMergeList[0].m_nHeight)+itCurrentRECT->top)/recContainer.bottom;
	
			//add a tile onto the tile list
			newTexture.m_TileList.push_back(newTile);

			++itTiles;
		}

		//clear textures in main texture list
		if(bClearTextures==true)
		{
			int nTexIndex=BinarySearch(m_lstMainTextureList,m_lstMergeList[0].m_sName);
			if(nTexIndex!=-1)
			{
				//release textures and set texture pointer to null
				if(m_lstMainTextureList[nTexIndex].m_texture)
					m_lstMainTextureList[nTexIndex].m_texture->Release();
				m_lstMainTextureList[nTexIndex].m_texture=NULL;

				//clear tile list and remove loadedtexture object from main texture list
				m_lstMainTextureList[nTexIndex].m_TileList.clear();
				m_lstMainTextureList.erase(m_lstMainTextureList.begin()+nTexIndex);
			}
		}
		//set texture pointer to null
		m_lstMergeList[0].m_texture=NULL;

		//clear tile list and erase loadedtexture from merge list
		m_lstMergeList[0].m_TileList.clear();
		m_lstMergeList.erase(m_lstMergeList.begin());

		++itCurrentRECT;
	}
	pDestSurface->Release();

	//put texture into list
	m_lstMainTextureList.push_back(newTexture);

	m_lstMergeList.clear();
	lstHoles.clear();
	lstFilledRects.clear();
	m_dTotalArea=0;
	
	//add tile for whole texture automatically
	AddTile(sMergedTextureName,"MAIN",1,1,recContainer.right,recContainer.bottom);//top, left, right, bottom
}


other info - eg sorting structures used, etc
//in .h file

struct LOADEDTEXTURE
{
        IDirect3DTexture9 *m_texture;   //the texture
        string m_sName;			//the filename of the texture
        int m_nWidth;                   //width of the texture
        int m_nHeight;                  //height of the texture
	int m_nArea;                    //area of texture
	vector <TILE> m_TileList;       //List of tiles within a texture
};

//sort merge list by width then by height
struct compareLoadedTex 
{
	bool operator()(const LOADEDTEXTURE &a, const LOADEDTEXTURE &b) const 
	{
		/*if (a.m_nWidth > b.m_nWidth) return true;
		if (a.m_nWidth == b.m_nWidth) return a.m_nHeight > b.m_nHeight;
		return false;*/if I use this code there is not crash
			
		/*if (a.m_nArea > b.m_nArea) return true;
		if (a.m_nArea == b.m_nArea) return a.m_nHeight > b.m_nHeight;
		return false;*/if I use this code there is a crash
	}
};

//compare holes when merging textures
struct compareHoleRects
{
	bool operator() (const RECT &recA, const RECT &recB)
	{
		if (recA.top < recB.top) return true;
		if (recA.top == recB.top) return recA.left < recB.left;
		return false;
	}
};

//return greatest value
struct compareLeastRight 
{
	bool operator()(const RECT &recA, const RECT &recB) const 
	{
		return recA.right < recB.right;
	}
};

//return least value
struct compareGreatestLeft 
{
	bool operator()(const RECT &recA, const RECT &recB) const 
	{
		return recA.left > recB.left;
	}
};


[Edited by - utilae on May 16, 2007 3:25:55 AM]

Share this post


Link to post
Share on other sites
Advertisement
.erase return the next valid iterator. A valid way of using erase with looks like this:


std::vector<Poop> allMyPoop;

std::vector<Poop>::iterator it = allMyPoop.begin();
while ( it != allMyPoop.end() )
{
if ( shouldErase )
{
it = allMyPoop.erase(it);
}
else
{
++it;
}
}




By erasing an iterator from a vector you are invalidating all current iterators. hence the crash in your code after you erase( xxx.begin() ) and then increment your current iterator.

A vector is contiguous memory space. That means when you delete something from the beginning of the vector, all other elements must be copied one unit of memory closer to the beginning of the vector. For that reason, erasing elements close to the beginning of a vector is pretty much the most expensive thing you can do with a vector. Erasing from the end is cheap b/c nothing needs to be copied. For that reason, if ordering isn't important in your vector, just swap current with end, erase end and then just re-evaluate current.

-me

Share this post


Link to post
Share on other sites
Ok, so I have made a few changes, but I still get the crash even though the 'erase' section works as it should. This crash stems from changing the following sorting code:

from this (no crash):

struct compareLoadedTex
{
bool operator()(const LOADEDTEXTURE &a, const LOADEDTEXTURE &b) const
{
if (a.m_nWidth > b.m_nWidth) return true;
if (a.m_nWidth == b.m_nWidth) return a.m_nHeight > b.m_nHeight;
return false;
}
};

to this (crash):

struct compareLoadedTex
{
bool operator()(const LOADEDTEXTURE &a, const LOADEDTEXTURE &b) const
{
if (a.m_nArea > b.m_nArea) return true;
if (a.m_nArea == b.m_nArea) return a.m_nHeight > b.m_nHeight;
return false;
}
};

I guess the different sorting function exposes some kind of weakness in my code.


Anyone able to comb through and pick out any problems.

Edit: ok, so I have found the cause of my problem - an infinite loop.
I have noted this below in the code. Basically, a certain sorting method
causes rects to not fit in any holes for some scenarios.

Many thanks if anyone can help.


//in .cpp file

//Combine all textures in MergeList into one texture
void CTextureManager::MergeTextures(const string &sMergedTextureName,const bool &bClearTextures)
{
//sort m_lstMergeList, largest area first, smallest last
sort(m_lstMergeList.begin(),m_lstMergeList.end(),compareLoadedTex());

vector <RECT> lstFilledRects;//list of inserted textures to make up final merged texture
list <RECT> lstHoles;//holes in merged texture that have to be filled with textures

//use total area of all textures to be merged, to try and make the final texture as square as possible
double dTextureWidth=sqrt(m_dTotalArea);
if(dTextureWidth<32)
dTextureWidth=32;
else if(dTextureWidth>32 && dTextureWidth<64)
dTextureWidth=64;
else if(dTextureWidth>64 && dTextureWidth<128)
dTextureWidth=128;
else if(dTextureWidth>128 && dTextureWidth<256)
dTextureWidth=256;
else if(dTextureWidth>256 && dTextureWidth<512)
dTextureWidth=512;
else if(dTextureWidth>512 && dTextureWidth<1024)
dTextureWidth=1024;
else if(dTextureWidth>1024)
dTextureWidth=2048;
//could put in more for bigger textures

//the first hole is the container, the final size of the merged texture
RECT recContainer;
recContainer.top=0;
recContainer.left=0;
recContainer.right=dTextureWidth;
recContainer.bottom=dTextureWidth;
lstHoles.push_back(recContainer);

//put every texture to merge in a hole until there are no more holes or all textures have been placed
list<RECT>::iterator itHole=lstHoles.begin();
list<RECT>::iterator itHoleTemp=lstHoles.begin();
vector<LOADEDTEXTURE>::iterator itTexture=m_lstMergeList.begin();
while(itTexture!=m_lstMergeList.end() && lstHoles.empty()==false)
{
//go back to first hole if there are still textures to put in holes
if(itHole==lstHoles.end())
itHole=lstHoles.begin();

//determine bin width and height
int nHoleHeight=itHole->bottom-itHole->top;
int nHoleWidth=itHole->right-itHole->left;

//case (e) or (f): texture too tall for bin or too wide for bin
if(itTexture->m_nHeight > nHoleHeight ||
itTexture->m_nWidth > nHoleWidth)
{
++itHole;//cannot fit in bin, try next hole
//edit - ok, so this becomes an infinite loop when
//rects cannot fit in any of the holes
//it happens with the sorting method used
//marked as 'crash' (discussed above in this post)
}
else //can fit
{
//sort list of holes
lstHoles.sort(compareHoleRects());

//will fit in bin so put texture in top left
RECT newFilledRect;
newFilledRect.top=itHole->top;
newFilledRect.left=itHole->left;
newFilledRect.right=itHole->left+itTexture->m_nWidth;
newFilledRect.bottom=itHole->top+itTexture->m_nHeight;
lstFilledRects.push_back(newFilledRect);

//update lowest bound of texture usage
if(newFilledRect.bottom>recContainer.bottom)
recContainer.bottom=newFilledRect.bottom;

//add replacement holes if there are any
RECT newHole;
//case (a): Perfect width. Too short.
if(itTexture->m_nWidth==nHoleWidth)
{
//case (a): Perfect width. Too short.
if(itTexture->m_nHeight<nHoleHeight)
{
//one hole below
newHole.top=itHole->top+itTexture->m_nHeight;
newHole.left=itHole->left;
newHole.right=itHole->right;
newHole.bottom=itHole->bottom;
lstHoles.push_back(newHole);
}
//case (d): Perfect width and height.
else if(itTexture->m_nHeight==nHoleHeight)
{
//no holes
}
}
else if(itTexture->m_nWidth<nHoleWidth)
{
//case (b): Too narrow. Too short.
if(itTexture->m_nHeight<nHoleHeight)
{
//one hole below
newHole.top=itHole->top+itTexture->m_nHeight;
newHole.left=itHole->left;
newHole.right=itHole->left+itTexture->m_nWidth;
newHole.bottom=itHole->bottom;
lstHoles.push_back(newHole);

//one hole to right
newHole.top=itHole->top;
newHole.left=itHole->left+itTexture->m_nWidth;
newHole.right=itHole->right;
newHole.bottom=itHole->bottom;
lstHoles.push_back(newHole);
}
//case (c): Too narrow. Perfect height.
else if(itTexture->m_nHeight==nHoleHeight)
{
//one hole to right
newHole.top=itHole->top;
newHole.left=itHole->left+itTexture->m_nWidth;
newHole.right=itHole->right;
newHole.bottom=itHole->bottom;
lstHoles.push_back(newHole);
}
}

//remove old hole
itHole=lstHoles.erase(itHole);

++itTexture;
}
}

//create texture the size of 'container'
LOADEDTEXTURE newTexture;
HRESULT hrCreateTex=D3DXCreateTexture(g_pD3DDevice9,recContainer.right,recContainer.bottom,D3DX_DEFAULT,0,D3DFMT_A8R8G8B8,D3DPOOL_MANAGED,&newTexture.m_texture);
g_Log<<"MergeTextures("<<sMergedTextureName<<") Create Texture: "<<DXGetErrorString9(hrCreateTex)<<endl;

//setup surfaces for copying textures
LPDIRECT3DSURFACE9 pSrcSurface=NULL;
LPDIRECT3DSURFACE9 pDestSurface=NULL;
newTexture.m_texture->GetSurfaceLevel(0,&pDestSurface);

//get correct width and height of texture (incase texture size is adjusted automatically when created)
D3DSURFACE_DESC surfaceDesc;
newTexture.m_texture->GetLevelDesc(0,&surfaceDesc);
recContainer.right=surfaceDesc.Width;
recContainer.bottom=surfaceDesc.Height;

//store textures filename, height and width
newTexture.m_sName=sMergedTextureName;
newTexture.m_nHeight=recContainer.bottom;
newTexture.m_nWidth=recContainer.right;
newTexture.m_nArea=recContainer.bottom*recContainer.right;

//go through list of textures and filled rects and copy textures to container texture
sort(m_lstMainTextureList.begin(),m_lstMainTextureList.end(),SortByNameAscending<LOADEDTEXTURE>());

vector<RECT>::iterator itCurrentRECT=lstFilledRects.begin();
vector<TILE>::iterator itTiles;
while(itCurrentRECT!=lstFilledRects.end())
{
//copy from texture to container texture
m_lstMergeList[0].m_texture->GetSurfaceLevel(0,&pSrcSurface);
D3DXLoadSurfaceFromSurface(pDestSurface,NULL,&(*itCurrentRECT),pSrcSurface,NULL,NULL,D3DX_FILTER_NONE,0);
pSrcSurface->Release();

//add tiles to merged texture, from each texture
itTiles=m_lstMergeList[0].m_TileList.begin();
while(itTiles!=m_lstMergeList[0].m_TileList.end())
{
//recalculate tile coordinates
TILE newTile;
newTile.m_sName=itTiles->m_sName;
newTile.m_tvTop=(float)((itTiles->m_tvTop*m_lstMergeList[0].m_nHeight)+itCurrentRECT->top)/recContainer.bottom;
newTile.m_tuLeft=(float)((itTiles->m_tuLeft*m_lstMergeList[0].m_nWidth)+itCurrentRECT->left)/recContainer.right;
newTile.m_tuRight=(float)((itTiles->m_tuRight*m_lstMergeList[0].m_nWidth)+itCurrentRECT->left)/recContainer.right;
newTile.m_TvBottom=(float)((itTiles->m_TvBottom*m_lstMergeList[0].m_nHeight)+itCurrentRECT->top)/recContainer.bottom;

//add a tile onto the tile list
newTexture.m_TileList.push_back(newTile);

++itTiles;
}

//clear textures in main texture list
if(bClearTextures==true)
{
int nTexIndex=BinarySearch(m_lstMainTextureList,m_lstMergeList[0].m_sName);
if(nTexIndex!=-1)
{
//release textures and set texture pointer to null
if(m_lstMainTextureList[nTexIndex].m_texture)
m_lstMainTextureList[nTexIndex].m_texture->Release();
m_lstMainTextureList[nTexIndex].m_texture=NULL;

//clear tile list and remove loadedtexture object from main texture list
m_lstMainTextureList[nTexIndex].m_TileList.clear();
m_lstMainTextureList.erase(m_lstMainTextureList.begin()+nTexIndex);
}
}
//set texture pointer to null
m_lstMergeList[0].m_texture=NULL;

//clear tile list and erase loadedtexture from merge list
m_lstMergeList[0].m_TileList.clear();
m_lstMergeList.erase(m_lstMergeList.begin());

++itCurrentRECT;
}
pDestSurface->Release();

//put texture into list
m_lstMainTextureList.push_back(newTexture);

m_lstMergeList.clear();
lstHoles.clear();
lstFilledRects.clear();
m_dTotalArea=0;

//add tile for whole texture automatically
AddTile(sMergedTextureName,"MAIN",1,1,recContainer.right,recContainer.bottom);//top, left, right, bottom
}




other info - eg sorting structures used, etc

//in .h file

struct LOADEDTEXTURE
{
IDirect3DTexture9 *m_texture; //the texture
string m_sName; //the filename of the texture
int m_nWidth; //width of the texture
int m_nHeight; //height of the texture
int m_nArea; //area of texture
vector <TILE> m_TileList; //List of tiles within a texture
};

//sort merge list by width then by height
struct compareLoadedTex
{
bool operator()(const LOADEDTEXTURE &a, const LOADEDTEXTURE &b) const
{
/*if (a.m_nWidth > b.m_nWidth) return true;
if (a.m_nWidth == b.m_nWidth) return a.m_nHeight > b.m_nHeight;
return false;*/
if I use this code there is not crash

/*if (a.m_nArea > b.m_nArea) return true;
if (a.m_nArea == b.m_nArea) return a.m_nHeight > b.m_nHeight;
return false;*/
if I use this code there is a crash
}
};

//compare holes when merging textures
struct compareHoleRects
{
bool operator() (const RECT &recA, const RECT &recB)
{
if (recA.top < recB.top) return true;
if (recA.top == recB.top) return recA.left < recB.left;
return false;
}
};

//return greatest value
struct compareLeastRight
{
bool operator()(const RECT &recA, const RECT &recB) const
{
return recA.right < recB.right;
}
};

//return least value
struct compareGreatestLeft
{
bool operator()(const RECT &recA, const RECT &recB) const
{
return recA.left > recB.left;
}
};





[Edited by - utilae on May 17, 2007 4:32:12 AM]

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!