cocos2d-x v3.3 unityちゃんのfbxデータを変換する際のメモ

正確なバージョンは把握してないのですが、v3.1あたりからCamera3D Testが追加され3Dモデルの表示テストができるようになってます。
これにunityちゃんfbxデータをc3bにして表示しようとすると、表示自体はできるのですが数点問題が発生したので、その対処方法のメモ。上手くいくと以下な感じで表示できます。
![unitychan02.png](https://qiita-image-store.s3.amazonaws.com/0/49333/71a9d9ed-90f6-6305-9b07-85e6c74654f3.png)

##unityのサイトからunityちゃんのデータを入手
今回はSD版のunityちゃんデータを利用します。
http://unity-chan.com/contents/guideline/?id=SDUnityChan

##unitypackageから以下を参考にfbxを取り出す
落としたデータはunitypackageなので、以下を参考にfbxを取り出します。
http://unrealengine.hatenablog.com/entry/2014/04/15/184432
複数のfbxが含まれますが、SD_unitychan_generic.fbxを利用します。

##fbx-convにてc3bに変換
fbx-convはcocos2d-xに付属しており、tools\fbx-conv\win32以下にあります。（mac版は未検証です）
fbx-conv.exe -f SD_unitychan_generic.fbx
テクスチャのuvが反転しているので、-fオプションを指定します。
変換時にWARNINGが表示されますが、これでSD_unitychan_generic.c3bができるはず。

##Sprite3DTestを改変する
Node: Camera3D Testでunityちゃんが表示されるようにSprite3DTestへ手を加えます。
Sprite3DTestのc3b読み込み部分を修正。

````Sprite3DTest.cpp
	addNewSpriteWithCoords( Vec3(0,0,0),"Sprite3DTest/SD_unitychan_generic.c3b",false,0.2f,true);
````
SD_unitychan_generic.c3bファイルと、fbxで利用されているtgaファイルをcpp-tests\Resources\Sprite3DTestへコピー。
これでSD_unitychan_generic.c3b自体が表示されるようになります。
が、以下のようになっているはずです。
![unitychan01.png](https://qiita-image-store.s3.amazonaws.com/0/49333/a41e866f-4462-e4c9-a3ab-b1665d2bbd67.png)

##unityちゃんfbxをfbx-convする際の問題点
問題としては２点あります。
### 顔のモデルがスキニングされておらず、表示位置がおかしくなる
顔のモデルである\_faceノードがCharacter1_Headの子になっていますが、スキニングされていないため、原点位置に表示されます。
### Cgfxシェーダーが利用されている箇所が赤くなる
cocos2d-xはcgfxには対応していないため、fbx-conv時にdiffuse(1,0,0)のマテリアルに置き換わります。
fbx-conv時に表示されている
WARNING:  [nol_mat] Material doesn't extend FbxSurfaceLambert, replaced with RED diffuse
は、このマテリアル置き換えを指しています。

## 問題点への対処方法
前述２つの問題ですが、fbx-convを改変して対処できます。
fbx-convのビルドは以下を参考に。
http://qiita.com/makanai/items/ae7776aef0147c30cc7e

### スキニングされていないmodelについて、強制的にスキニングする
顔のモデルがスキニングされておらず、表示位置がおかしくなる問題について、
スキニングされていないmodelは強制的にスキニングを行って対処します。

````FbxConvert.h
		void prefetchMeshes() {
			{
				int cnt = scene->GetGeometryCount();
				FbxGeometryConverter converter(manager);
				std::vector<FbxGeometry *> triangulate;
				for (int i = 0; i < scene->GetGeometryCount(); ++i) {
					FbxGeometry * geometry = scene->GetGeometry(i);
					if (!geometry->Is<FbxMesh>() || !((FbxMesh*)geometry)->IsTriangleMesh())
						triangulate.push_back(geometry);
				}
				for (std::vector<FbxGeometry *>::iterator it = triangulate.begin(); it != triangulate.end(); ++it)
				{
					log->status(log::sSourceConvertFbxTriangulate, getGeometryName(*it), (*it)->GetClassId().GetName());
					FbxNodeAttribute * const attr = converter.Triangulate(*it, true);
				}
			}

			int cnt = scene->GetGeometryCount();
			for (int i = 0; i < cnt; ++i) {
				FbxGeometry * geometry = scene->GetGeometry(i);

				if (fbxMeshMap.find(geometry) == fbxMeshMap.end()) {
					if (!geometry->Is<FbxMesh>()) {
						log->warning(log::wSourceConvertFbxCantTriangulate, geometry->GetClassId().GetName());
						continue;
					}
					FbxMesh *mesh = (FbxMesh*)geometry;
					int indexCount = (mesh->GetPolygonCount() * 3);
					log->verbose(log::iSourceConvertFbxMeshInfo, getGeometryName(mesh), mesh->GetPolygonCount(), indexCount, mesh->GetControlPointsCount());
					if (indexCount > settings->maxIndexCount)
						log->warning(log::wSourceConvertFbxExceedsIndices, indexCount, settings->maxIndexCount);
					if (mesh->GetElementMaterialCount() <= 0) {
						log->error(log::wSourceConvertFbxNoMaterial, getGeometryName(mesh));
						continue;
					}

					//スキニングされてない場合、親ノードへ強制的にスキニングする
					if (mesh->GetDeformerCount(FbxDeformer::eSkin)==0){

						std::function<FbxNode*(FbxNode* node, FbxGeometry* geo)> findGeometry = [&findGeometry](FbxNode* node, FbxGeometry* geo)->FbxNode*{
							if (node->GetGeometry() == geo)return node;
							for (int i = 0; i < node->GetChildCount(); i++){
								auto n = findGeometry(node->GetChild(i), geo);
								if (n)return n;
							}
							return nullptr;
						};

						FbxNode* nChild = findGeometry(scene->GetRootNode(), geometry);
						FbxNode* n = nChild->GetParent();
						n->RemoveChild(nChild);
						scene->GetRootNode()->AddChild(nChild);
						FbxAnimEvaluator* mySceneEvaluator = scene->GetAnimationEvaluator();
						auto mBase = mySceneEvaluator->GetNodeGlobalTransform(n);
						auto m = mBase.Inverse();

						FbxCluster *lClusterToRoot = FbxCluster::Create(scene, "");
						lClusterToRoot->SetLink(n);
						lClusterToRoot->SetTransformLinkMatrix(mBase);
						lClusterToRoot->SetLinkMode(FbxCluster::eTotalOne);
						for (int j = 0; j < mesh->GetControlPointsCount(); ++j){
							lClusterToRoot->AddControlPointIndex(j, 1.0f);
						}
						FbxSkin* lSkin = FbxSkin::Create(scene, "");
						lSkin->AddCluster(lClusterToRoot);
						mesh->AddDeformer(lSkin);
					}
					FbxMeshInfo * const info = new FbxMeshInfo(log, mesh, settings->packColors, settings->maxVertexBonesCount, settings->forceMaxVertexBoneCount, settings->maxNodePartBonesCount);
					meshInfos.push_back(info);
					fbxMeshMap[mesh] = info;
					if (info->bonesOverflow)
						log->warning(log::wSourceConvertFbxExceedsBones);
				}
				else {
					log->warning(log::wSourceConvertFbxDuplicateMesh, getGeometryName(geometry));
				}
			}
		}
````

### cgfx内のテクスチャ名をdiffuseに使用する
Cgfxシェーダー利用時にマテリアル置き換えしている箇所を改変し、Cgfx中のテクスチャをdiffuseとしてアサインするようにします。
unityちゃんではDiffuseTextureというテクスチャがdiffuseに該当しているので、それを利用します。

````FbxConvert.h
		Material *createMaterial(FbxSurfaceMaterial * const &material) {	
			Material * const result = new Material();
			result->source = material;
			result->id = material->GetName();
            
			if ((!material->Is<FbxSurfaceLambert>()) || GetImplementation(material, FBXSDK_IMPLEMENTATION_HLSL) || GetImplementation(material, FBXSDK_IMPLEMENTATION_CGFX)) {
				if (!material->Is<FbxSurfaceLambert>()){
					log->warning(log::wSourceConvertFbxMaterialUnknown, result->id.c_str());
					float f[3] = { 1, 0, 0 };
					result->diffuse.set(f);
				}
				if (GetImplementation(material, FBXSDK_IMPLEMENTATION_HLSL)){
					log->warning(log::wSourceConvertFbxMaterialHLSL, result->id.c_str());
					float f[3] = { 1, 0, 0 };
					result->diffuse.set(f);
				}
				if (GetImplementation(material, FBXSDK_IMPLEMENTATION_CGFX)){
					log->warning(log::wSourceConvertFbxMaterialCgFX, result->id.c_str());
					float f[3] = { 1, 1, 1 };
					result->diffuse.set(f);

					//cgfxからdiffuseテクスチャ名を探す
					auto lImplementation = GetImplementation(material, FBXSDK_IMPLEMENTATION_CGFX);

					const FbxBindingTable* lRootTable = lImplementation->GetRootTable();
					FbxString lFileName = lRootTable->DescAbsoluteURL.Get();
					FbxString lTechniqueName = lRootTable->DescTAG.Get();

					const FbxBindingTable* lTable = lImplementation->GetRootTable();
					size_t lEntryNum = lTable->GetEntryCount();

					for (int i = 0; i < (int)lEntryNum; ++i)
					{
						const FbxBindingTableEntry& lEntry = lTable->GetEntry(i);
						const char* lEntrySrcType = lEntry.GetEntryType(true);
						FbxProperty lFbxProp;

						FbxString lTest = lEntry.GetSource();
						if (lTest.Find("DiffuseTexture") < 0)continue;

						if (strcmp(FbxPropertyEntryView::sEntryType, lEntrySrcType) == 0)
						{
							lFbxProp = material->FindPropertyHierarchical(lEntry.GetSource());
							if (!lFbxProp.IsValid())
							{
								lFbxProp = material->RootProperty.FindHierarchical(lEntry.GetSource());
							}
						}
						else if (strcmp(FbxConstantEntryView::sEntryType, lEntrySrcType) == 0)
						{
							lFbxProp = lImplementation->GetConstants().FindHierarchical(lEntry.GetSource());
						}
						if (lFbxProp.IsValid())
						{
							if (lFbxProp.GetSrcObjectCount<FbxTexture>() > 0)
							{
								for (int j = 0; j < lFbxProp.GetSrcObjectCount<FbxFileTexture>(); ++j)
								{
									FbxFileTexture *lTex = lFbxProp.GetSrcObject<FbxFileTexture>(j);
									add_if_not_null(result->textures, createTexture(lTex, Material::Texture::Diffuse));
								}
							}
						}
					}
				}
				return result;
			}
````

あとはfbx-convをビルドし、出来上がったexeで変換すれば問題が解決するはずです。
またフェイシャル以外のアニメーションはそのまま利用できたので、必要ならunitypackage付属のアニメーションを変換して利用すると良いかと思います。