Texture UVs on FBX mesh are foo bared

Started by
11 comments, last by L. Spiro 9 years, 11 months ago

I ventured into loading FBX Meshes do to the ability for morph targets, skeletal mesh animation and other neat stuff that we all love. However, tutorials on FBX SDK are hard to come by like finding the last milk in a apocolypic future store. I grabbed some samples from others and somehow miracably able to create a mesh. However, the UV's aren't right.

As sceen in the attachment. I looked at it closely and it appears to me that the UV's are using something other than the UV's. The code i have is:


for (int j = 0; j < meshArray.Size(); j++) {

			vCount = meshArray[j]->GetControlPointsCount();

			m_vertices = new myVertex[vCount];
			Face face;
			 polygonCount = meshArray[j]->GetPolygonVertexCount() / 3;
			numIndices = meshArray[j]->GetPolygonVertexCount();

			FbxVector4 *vert = meshArray[j]->GetControlPoints();
			int arrayIndex = 0;


			//memcpy(vert, meshArray[j]->GetControlPoints(), sizeof(FbxVector4)* vCount);


			for (int i = 0; i < polygonCount; i++) {
				for (unsigned int k = 0; k < 3; k++) {
					int index = 0;
					FbxVector4 fbxNorm(0, 0, 0, 0);
					FbxVector2 fbxUV(0, 0);
					FbxVector4 Vers(0, 0, 0, 0);

					FbxStringList UVStringNames;
					meshArray[j]->GetUVSetNames(UVStringNames);

					bool texCoordFound = false;
					face.indices[k] = index = meshArray[j]->GetPolygonVertex(i, k);
					Vers = vert[index];
					m_vertices[index].position = XMFLOAT4(Vers.mData[0],Vers.mData[2],Vers.mData[1],0.0f);
					
					meshArray[j]->GetPolygonVertexNormal(i, k, fbxNorm);
					m_vertices[index].normal = XMFLOAT3(fbxNorm.mData[0], fbxNorm.mData[2], fbxNorm.mData[1]);

					FbxLayerElementUV* fbxLayerUV = meshArray[j]->GetLayer(0)->GetUVs();
					if (fbxLayerUV) {
						int fbxUVIndex = 0;
						
						switch (fbxLayerUV->GetMappingMode())
						{
						case FbxLayerElement::eByControlPoint :
							fbxUVIndex = face.indices[k];
							break;
						case FbxLayerElement::eByPolygonVertex :
							fbxUVIndex = meshArray[j]->GetTextureUVIndex(i, k, FbxLayerElement::eTextureDiffuse);
							break;
						}
						fbxUV = fbxLayerUV->GetDirectArray().GetAt(fbxUVIndex);
					}
					m_vertices[index].texture = XMFLOAT2(fbxUV.mData[0], fbxUV.mData[1]);
	
				}
				Faces.push_back(face);

			}

		}

		int indexCount = Faces.size() * 3;
		indices = new unsigned long[indexCount];
		int indicie = 0;
		for (unsigned int i = 0; i < Faces.size(); ++i) {
			indices[indicie++] = Faces[i].indices[2];
			indices[indicie++] = Faces[i].indices[1];
			indices[indicie++] = Faces[i].indices[0];
		}
		Faces.clear();

Perhaps the indices aren't really well. I believe I switched the order of how the indices are drawn for some reason but can't remember why because I moved on to MD5 mesh and MD5 animation loading. However, unhappy with loading MD5 and want to continue looking into how I can correct the FBX mesh's texture coordiants.

I'll also look at the viewScene sample that came with the SDK as well in the meantime.

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX
Advertisement

Also I provided some visual debugging for the UV Textures in the shader. The incorrect one is the first one. The corrected UV are in my own model file format.

It appears to me I have the UV's reversed and the coloration in black is where the UV coords are messed up representing 0.0f perhaps?

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

Since you have access to the texture coordinate CPU side, break into the code or print the uv values while the model is being loaded to verify that they are on par with your loader. I've seen cases with 3DS Max, where the sign of the texture coordinate is flip due to wrapping...

Since you have access to the texture coordinate CPU side, break into the code or print the uv values while the model is being loaded to verify that they are on par with your loader. I've seen cases with 3DS Max, where the sign of the texture coordinate is flip due to wrapping...

Alright, I'm tweaking the code now to see if I see anything different. If not then I'll output to file.

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

Are you converting the uv origin? Your 3D package probably thinks the origin is the lower-left corner, whereas DX puts the origin at the upper-left corner:


for( auto& vertex : mesh.vertex_buffer )
{
     // other xforms...

     if( gSettings.FlipTexV )
     {
          vertex.texcoord.y = 1.0f - vertex.texcoord.y;
     }
}

And here's how I extract the uv from FBX:


                        //---------------------------------------------------------
			// uv
			//---------------------------------------------------------
			FbxVector2 uv;
			FbxGeometryElementUV* leUV = mpMesh->GetElementUV( 0 );
			if( leUV == nullptr )
			{
				ERRPRINT( "No UV layers in mesh. Aborting." );
				return false;
			}
			else
			{
				switch( leUV->GetMappingMode() )
				{
					case FbxGeometryElement::eByControlPoint:
					{
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							uv = leUV->GetDirectArray().GetAt( cp_index );
							break;
							case FbxGeometryElement::eIndexToDirect:
							{
								int id = leUV->GetIndexArray().GetAt( cp_index );
								uv = leUV->GetDirectArray().GetAt( id );
							}
							break;
							default:
								break;
						}
					}
					break;

					case FbxGeometryElement::eByPolygonVertex:
					{
						int lTextureUVIndex = mpMesh->GetTextureUVIndex( i, j );
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							case FbxGeometryElement::eIndexToDirect:
							{
								uv = leUV->GetDirectArray().GetAt( lTextureUVIndex );
							}
							break;
							default:
								break;
						}
					}
					default:
						break;
				}
				vertex.texcoord = Vec2FromFbx( &uv );
			}

Are you converting the uv origin? Your 3D package probably thinks the origin is the lower-left corner, whereas DX puts the origin at the upper-left corner:


for( auto& vertex : mesh.vertex_buffer )
{
     // other xforms...

     if( gSettings.FlipTexV )
     {
          vertex.texcoord.y = 1.0f - vertex.texcoord.y;
     }
}

And here's how I extract the uv from FBX:


                        //---------------------------------------------------------
			// uv
			//---------------------------------------------------------
			FbxVector2 uv;
			FbxGeometryElementUV* leUV = mpMesh->GetElementUV( 0 );
			if( leUV == nullptr )
			{
				ERRPRINT( "No UV layers in mesh. Aborting." );
				return false;
			}
			else
			{
				switch( leUV->GetMappingMode() )
				{
					case FbxGeometryElement::eByControlPoint:
					{
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							uv = leUV->GetDirectArray().GetAt( cp_index );
							break;
							case FbxGeometryElement::eIndexToDirect:
							{
								int id = leUV->GetIndexArray().GetAt( cp_index );
								uv = leUV->GetDirectArray().GetAt( id );
							}
							break;
							default:
								break;
						}
					}
					break;

					case FbxGeometryElement::eByPolygonVertex:
					{
						int lTextureUVIndex = mpMesh->GetTextureUVIndex( i, j );
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							case FbxGeometryElement::eIndexToDirect:
							{
								uv = leUV->GetDirectArray().GetAt( lTextureUVIndex );
							}
							break;
							default:
								break;
						}
					}
					default:
						break;
				}
				vertex.texcoord = Vec2FromFbx( &uv );
			}

I'll give it a shot - as far as origion. I did flip the V to 1.0f - texcoord.mData[1]

I'll try out your code in a bit and I'll let you know what's up. Why are FBX so hard to work with or is the lack of tutorials?

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

I have the Max 3DS SDK and I'm dying to know how to create export my own custom file format and put together what I desire. However, that's for another thread. I'll let you know what's going on and take more debug shots if needed, Scott.

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

Interestingly enough, I am able to write out the uV and I checked them with copy of the FBX as FBX ASCII file. Here's the UV in a text document the program wrote out:


0.709074,0.551956
0.709074,0.551956
0.709074,1
1,0.551956
0.418147,0.551956
0.418147,0.551956
0.418147,1
0.709074,0.551956

And here's the section where the UVS are in the mesh file:


LayerElementUV: 0 {
			Version: 101
			Name: "UVChannel_1"
			MappingInformationType: "ByPolygonVertex"
			ReferenceInformationType: "IndexToDirect"
			UV: *48 {
				a: 0.418147087097168,0,0.418147087097168,0.448043644428253,0,0.448043644428253,0,0,0.418147087097168,0.896087288856506,0,0.896087288856506,0,0.448043644428253,0.418147087097168,0.448043644428253,0.999999940395355,0,0.999999940395355,0.418147087097168,0.709073543548584,0.418147087097168,0.709073543548584,0,0.709073543548584,0.448043644428253,0.709073543548584,0.896087288856506,0.418147087097168,0.896087288856506,0.418147087097168,0.448043644428253,0.999999940395355,0.448043644428253,0.999999940395355,0.866190731525421,0.709073543548584,0.866190731525421,0.709073543548584,0.448043644428253,0.709073543548584,0,0.709073543548584,0.448043644428253,0.418147087097168,0.448043644428253,0.418147087097168,0
			} 
			UVIndex: *36 {
				a: 3,0,2,0,1,2,6,7,4,6,4,5,10,11,8,10,8,9,14,15,12,14,12,13,18,19,16,18,16,17,22,23,20,22,20,21
			} 
		}

Yeah, I have the V flipped. Maybe it's a index things?

I did what Scott said and checked the code to this: I'm confused on the whole cp_index though does he mean index what I have in my previous code?


FbxGeometryElementUV *uv = meshArray[j]->GetElementUV(0);
					if (uv == nullptr){
						//no texture uv detected.
						MessageBox(0, L"No UVs.", L"", 0);
						return false;
					}
					else {
						switch (uv->GetMappingMode())
						{
						case FbxGeometryElement::eByControlPoint:
						{
							switch (uv->GetReferenceMode())
							{
							case FbxGeometryElement::eDirect:
								fbxUV = uv->GetDirectArray().GetAt(index);
								
								break;
							case FbxGeometryElement::eIndexToDirect:{
								int id = uv->GetIndexArray().GetAt(index);
								fbxUV = uv->GetDirectArray().GetAt(id);

							}
								break;

							default:
								break;
							}
						}
							break;

						case FbxGeometryElement::eByPolygonVertex:
						{
							int lTextureUVIndex = meshArray[j]->GetTextureUVIndex(i, k);
							switch (uv->GetReferenceMode())
							{
							case FbxGeometryElement::eDirect:
								break;
							case FbxGeometryElement::eIndexToDirect: {
								fbxUV = uv->GetDirectArray().GetAt(lTextureUVIndex);

							} break;

							}

						}
						default:
							break;
						}
					}
					m_vertices[index].texture = XMFLOAT2(fbxUV.mData[0], 1.0f - fbxUV.mData[1]);

				}
				Faces.push_back(face);

			}

		}

		int indexCount = Faces.size() * 3;
		indices = new unsigned long[indexCount];
		int indicie = 0;
		for (unsigned int i = 0; i < Faces.size(); ++i) {
			indices[indicie++] = Faces[i].indices[2];
			indices[indicie++] = Faces[i].indices[1];
			indices[indicie++] = Faces[i].indices[0];
		}
		Faces.clear();

It's a possible indexing issue, yes?

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

Apologies; the cp_index is part of the overall loop of how I extract the data. I only posted the UV snippet above. Here's the whole thing for how I extract the vertex data and the subset of material properties I care about:


// This is where things start to get nasty...

bool Import_FBX::ExtractGeometry()
{
	// control points
	uint32 countCP = mpMesh->GetControlPointsCount();
	control_points.reserve( countCP );
	FbxVector4* ControlPoints = mpMesh->GetControlPoints();

	for( uint32 i = 0; i < countCP; i++ )
	{
		control_points.push_back( Vec3FromFbx( &ControlPoints[i] ) );
	}

	// polygons
	uint32 countPolys = mpMesh->GetPolygonCount();
	polygons.reserve( countPolys );

	int vertexID = 0;
	for( uint32 i = 0; i < countPolys; i++ )
	{
		xPolygon polygon;

		uint32 countPolyVerts = mpMesh->GetPolygonSize( i );
		if( countPolyVerts < 3 || countPolyVerts > 4 ) // only tris and quads for now
		{
			ERRPRINT("Found poly (index %d) with unsupported vertex count: %d", i, countPolyVerts);
			return false;
		}
		for( uint32 j = 0; j < countPolyVerts; j++ )
		{
			vertex_type vertex;

			//---------------------------------------------------------
			// position
			//---------------------------------------------------------
			int cp_index = mpMesh->GetPolygonVertex( i, j );
			FbxVector4 cp = ControlPoints[cp_index];
			vertex.position = Vec3FromFbx( &cp );


			//---------------------------------------------------------
			// normal
			//---------------------------------------------------------
			FbxVector4 fbxNormal;
			FbxGeometryElementNormal* leNormal = mpMesh->GetElementNormal( 0 );
			if( leNormal == nullptr )
			{
//				ERRPRINT("No normals in mesh");
			}
			else
			{
				FbxLayerElement::EMappingMode normals_MapMode = leNormal->GetMappingMode();
				if( normals_MapMode == FbxGeometryElement::eByPolygonVertex )
				{
					switch( leNormal->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxNormal = leNormal->GetDirectArray().GetAt( vertexID );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leNormal->GetIndexArray().GetAt( vertexID );
						fbxNormal = leNormal->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				else if( normals_MapMode == FbxGeometryElement::eByControlPoint )
				{
					switch( leNormal->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxNormal = leNormal->GetDirectArray().GetAt( cp_index );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leNormal->GetIndexArray().GetAt( cp_index );
						fbxNormal = leNormal->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				vertex.normal = Vec3FromFbx( &fbxNormal );
			}

			//---------------------------------------------------------
			// tangent
			//---------------------------------------------------------
			FbxVector4 fbxTangent;
			FbxGeometryElementTangent* leTangent = mpMesh->GetElementTangent( 0 );
			if( leTangent == nullptr )
			{
//				ERRPRINT("No tangents in mesh");
			}
			else
			{
				FbxLayerElement::EMappingMode tangent_MapMode = leTangent->GetMappingMode();
				if( tangent_MapMode == FbxGeometryElement::eByPolygonVertex )
				{
					switch( leTangent->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxTangent = leTangent->GetDirectArray().GetAt( vertexID );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leTangent->GetIndexArray().GetAt( vertexID );
						fbxTangent = leTangent->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				else if( tangent_MapMode == FbxGeometryElement::eByControlPoint )
				{
					switch( leTangent->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxTangent = leTangent->GetDirectArray().GetAt( cp_index );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leTangent->GetIndexArray().GetAt( cp_index );
						fbxTangent = leTangent->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				vertex.tangent = Vec3FromFbx( &fbxTangent );
			}



			//---------------------------------------------------------
			// binormal
			//---------------------------------------------------------
			FbxVector4 fbxBinormal;
			FbxGeometryElementBinormal* leBinormal = mpMesh->GetElementBinormal( 0 );
			if( leBinormal == nullptr )
			{
//				ERRPRINT( "No binormals in mesh" );
			}
			else
			{
				FbxLayerElement::EMappingMode binormal_MapMode = leTangent->GetMappingMode();
				if( binormal_MapMode == FbxGeometryElement::eByPolygonVertex )
				{
					switch( leBinormal->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxBinormal = leBinormal->GetDirectArray().GetAt( vertexID );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leBinormal->GetIndexArray().GetAt( vertexID );
						fbxBinormal = leBinormal->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				else if( binormal_MapMode == FbxGeometryElement::eByControlPoint )
				{
					switch( leBinormal->GetReferenceMode() )
					{
					case FbxGeometryElement::eDirect:
						fbxBinormal = leBinormal->GetDirectArray().GetAt( cp_index );
						break;
					case FbxGeometryElement::eIndexToDirect:
					{
						int id = leBinormal->GetIndexArray().GetAt( cp_index );
						fbxBinormal = leBinormal->GetDirectArray().GetAt( id );
					}
						break;
					default:
						break;
					}
				}
				vertex.binormal = Vec3FromFbx( &fbxBinormal );
			}



			//---------------------------------------------------------
			// uv
			//---------------------------------------------------------
			FbxVector2 uv;
			FbxGeometryElementUV* leUV = mpMesh->GetElementUV( 0 );
			if( leUV == nullptr )
			{
				ERRPRINT( "No UV layers in mesh. Aborting." );
				return false;
			}
			else
			{
				switch( leUV->GetMappingMode() )
				{
					case FbxGeometryElement::eByControlPoint:
					{
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							uv = leUV->GetDirectArray().GetAt( cp_index );
							break;
							case FbxGeometryElement::eIndexToDirect:
							{
								int id = leUV->GetIndexArray().GetAt( cp_index );
								uv = leUV->GetDirectArray().GetAt( id );
							}
							break;
							default:
								break;
						}
					}
					break;

					case FbxGeometryElement::eByPolygonVertex:
					{
						int lTextureUVIndex = mpMesh->GetTextureUVIndex( i, j );
						switch( leUV->GetReferenceMode() )
						{
							case FbxGeometryElement::eDirect:
							case FbxGeometryElement::eIndexToDirect:
							{
								uv = leUV->GetDirectArray().GetAt( lTextureUVIndex );
							}
							break;
							default:
								break; 
						}
					}
					default:
						break;
				}
				vertex.texcoord = Vec2FromFbx( &uv );
			}
			polygon.vertices.push_back( vertex );
			++vertexID;
		}

		polygon.vertex_count = polygon.vertices.size();
		polygons.push_back( polygon );
	}
	return true;
}

bool Import_FBX::ExtractMaterials()
{
	//------------------------------------------------------------
	// Initialize material list and get material index per polygon
	//------------------------------------------------------------
	int lMtrlCount = mpNode->GetMaterialCount();
	for( int i = 0; i < lMtrlCount; i++ )
	{
		SerializedMaterial m;
		m.id = i;
		materials.push_back( m );
	}
	for( int l = 0; l < mpMesh->GetElementMaterialCount(); l++ )
	{
		FbxGeometryElementMaterial* leMat = mpMesh->GetElementMaterial( l );
		if( leMat != nullptr )
		{
			if( leMat->GetReferenceMode() == FbxGeometryElement::eIndex || leMat->GetReferenceMode() == FbxGeometryElement::eIndexToDirect )
			{
				int lIndexArrayCount = leMat->GetIndexArray().GetCount();
				for( int i = 0; i < lIndexArrayCount; i++ )
				{
					polygons[i].material_id = leMat->GetIndexArray().GetAt( i );
				}
			}
		}
	}

	//----------------------------------------------------------------
	// From each material extract the diffuse texture and uv repeat
	// The rest of the maps and properties will be set in our own tool
	//----------------------------------------------------------------
	int lMaterialIndex;
    FbxProperty lProperty;
	int lNbMat = mpNode->GetSrcObjectCount<FbxSurfaceMaterial>();
	for( lMaterialIndex = 0; lMaterialIndex < lNbMat; lMaterialIndex++ )
	{
		FbxSurfaceMaterial *lMaterial = mpNode->GetSrcObject<FbxSurfaceMaterial>( lMaterialIndex );
		if( lMaterial != nullptr )
		{
			SerializedMaterial& material = materials[lMaterialIndex];
			strcpy( material.tex_0, "default.dds" );
			material.texture_flags |= tex_Diffuse;

			lProperty = lMaterial->FindProperty( FbxLayerElement::sTextureChannelNames[0] );
			cstring propertyName = lProperty.GetNameAsCStr();
			propertyName.makeLower();
			if( propertyName == "diffusecolor" )
			{
				FbxTexture* lTexture = lProperty.GetSrcObject<FbxTexture>( 0 );
				if( lTexture )
				{
					FbxFileTexture *lFileTexture = FbxCast<FbxFileTexture>( lTexture );
					if( lFileTexture != nullptr )
					{
						//TODO: Revisit when archives are reworked
						cstring texname1 = lFileTexture->GetRelativeFileName();
						cstring texname2 = lFileTexture->GetFileName();

						strcpy( material.tex_0, texname1.c_str() );
						material.map_scales.x = static_cast<float>( lFileTexture->GetScaleU() );
						material.map_scales.y = static_cast<float>( lFileTexture->GetScaleV() );
					}
				}
			}
		}
	}
}

xPolygon is just a simple struct that contains the vertex array for that poly and some helper functions for calculating tangents, centroid; stuff like that. Basically I extract the FBX into my own list of polys, do whatever processing I want on them, and then create the per-material submeshes I'll write out to the model format, from that list of polys.

I'll have to check out your code. Of what I've understand is that you get the control points which are the vertices inside the mesh. You get the total, reserve a vector or some type of stl container then stuff the control points inside that container. When down to the nitty gritty of getting the polygon count size whether the mesh is quad or triangulated. The local variable index or cp_index which is where the index of the vertice's (control points) are...Responsible of getting the normals, bitangents, texture uv's. So, polygon is indices in FBX terms, right? so for instance myMesh->GetPolygonSize() would return the number of indices or would it be more myMesh->GetPolygonVertexCount() to receive indice count or the number of polygons inside the mesh?

I think control points and polygons are tripping me a bit because to me loading a simple box FBX mesh isn't like loading a OBJ file. I understand it's composed of vertices, normals, tangents, bitangents and uv maps as in every model format kind of similar does. Tangents and bitangents have to be calculated when loading OBJ files.

Anyways, I'm going to have a look at your code - my brain has to cool down from storing a lot new information.

Game Engine's WIP Videos - http://www.youtube.com/sicgames88
SIC Games @ GitHub - https://github.com/SICGames?tab=repositories
Simple D2D1 Font Wrapper for D3D11 - https://github.com/SICGames/D2DFontX

This topic is closed to new replies.

Advertisement