示例#1
0
Bone* BuildSkeleton(aiNode& aiNode, Model& model, Bone* parent)
{
	Bone* bone = nullptr;

	auto iter = model.mBoneIndexMap.find(aiNode.mName.C_Str());
	if(iter == model.mBoneIndexMap.end())
	{
		bone = new Bone();
		if(aiNode.mName.length > 0)
		{
			bone->name = aiNode.mName.C_Str();
		}
		else
		{
			static u32 counter = 0;
			char buffer[128];
			sprintf_s(buffer, 128, "Unknown%d", counter++);
			bone->name = buffer;
		}
		bone->index = model.mBones.size();

		model.mBones.push_back(bone);
		model.mBoneIndexMap.insert(std::make_pair(bone->name, bone->index));
	}
	else
	{
		bone = model.mBones[iter->second];
	}

	bone->transform = *(Math::Matrix*)&(aiNode.mTransformation);
	bone->parent = parent;
	if (parent != nullptr)
	{
		bone->parentIndex = parent->index;
	}

	for(u32 i = 0; i < aiNode.mNumChildren; ++i)
	{
		Bone* child = BuildSkeleton(*(aiNode.mChildren[i]), model, bone);
		bone->children.push_back(child);
	}

	return bone;
}
示例#2
0
void ImportModel(const char* infileName, const char* outfileName)
{
	Assimp::Importer importer; 
	u32 flags = aiProcess_Triangulate |
				aiProcess_JoinIdenticalVertices |
				aiProcess_SortByPType |
				aiProcess_FlipUVs;

	const aiScene* scene = importer.ReadFile(infileName, flags);
	ASSERT(scene, "Failed to load model - %s", importer.GetErrorString());

	FILE* pFile = nullptr;
	fopen_s(&pFile, outfileName, "w");

	fprintf_s(pFile, "MeshCount: %d\n", scene->mNumMeshes);
	fprintf_s(pFile, "TextureCount: %d\n", scene->mNumMaterials);	
	
	Model model;

	if(scene->HasMeshes())
	{
		for(u32 meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex)
		{
			aiMesh* aiMesh = scene->mMeshes[meshIndex];
			fprintf_s(pFile, "VertexCount: %d\n", aiMesh->mNumVertices);
			fprintf_s(pFile, "IndexCount: %d\n", aiMesh->mNumFaces * 3);
			
			Mesh* mesh = new Mesh();

			Mesh::Vertex* vertices = new Mesh::Vertex[aiMesh->mNumVertices];
			Mesh::Vertex* vertexIter = vertices;
			for(u32 i = 0; i < aiMesh->mNumVertices; ++i)
			{
				fprintf_s(pFile, "%f %f %f ", aiMesh->mVertices[i].x, aiMesh->mVertices[i].y, aiMesh->mVertices[i].z);// * 0.1f;
				if(aiMesh->HasNormals())
				{
					fprintf_s(pFile, "%f %f %f\n", aiMesh->mNormals[i].x, aiMesh->mNormals[i].y, aiMesh->mNormals[i].z);// * 0.1f;
				}
				else
				{
					fprintf_s(pFile, "\n");
				}

				
				vertexIter->position.x = aiMesh->mVertices[i].x;// * 0.1f;
				vertexIter->position.y = aiMesh->mVertices[i].y;// * 0.1f;
				vertexIter->position.z = aiMesh->mVertices[i].z;// * 0.1f;
				++vertexIter;

			}

			if(aiMesh->HasNormals())
			{
				vertexIter = vertices;
				for(u32 i = 0; i < aiMesh->mNumVertices; ++i)
				{
					vertexIter->normal.x = aiMesh->mNormals[i].x;
					vertexIter->normal.y = aiMesh->mNormals[i].y;
					vertexIter->normal.z = aiMesh->mNormals[i].z;
					++vertexIter;
				}
			}

			const u32 uvChannelCount = aiMesh->GetNumUVChannels();
			for (u32 i = 0; i < uvChannelCount; ++i)
			{
				Mesh::Vertex* vertexIter = vertices;
				for (u32 j = 0; j < aiMesh->mNumVertices; ++j)
				{
					fprintf_s(pFile, "%f %f\n", aiMesh->mTextureCoords[i][j].x, aiMesh->mTextureCoords[i][j].y);
					
					vertexIter->texcoord.x = aiMesh->mTextureCoords[i][j].x;
					vertexIter->texcoord.y = aiMesh->mTextureCoords[i][j].y;
					++vertexIter;
				}

				// TODO - Only get the first set of uv for now
				break;
			}

			u16* indices = new u16[aiMesh->mNumFaces * 3];
			u16* indexIter = indices;

			for(u32 i = 0; i < aiMesh->mNumFaces; ++i)
			{
				fprintf_s(pFile, "%d %d %d\n", aiMesh->mFaces[i].mIndices[0], aiMesh->mFaces[i].mIndices[1], aiMesh->mFaces[i].mIndices[2]);

				indexIter[0] = aiMesh->mFaces[i].mIndices[0];
				indexIter[1] = aiMesh->mFaces[i].mIndices[1];
				indexIter[2] = aiMesh->mFaces[i].mIndices[2];
				indexIter += 3;
			}

			MeshBuilder::GenerateMesh(*mesh, vertices, aiMesh->mNumVertices, indices, aiMesh->mNumFaces * 3);
			model.mMeshes.push_back(mesh);

			SafeDeleteArray(vertices);
			SafeDeleteArray(indices);

			if(aiMesh->HasBones())
			{
				VertexWeights& vertexWeights = mesh->GetVertexWeights();
				vertexWeights.resize(aiMesh->mNumVertices);

				for(u32 i = 0; i < aiMesh->mNumBones; ++i)
				{
					aiBone* aiBone = aiMesh->mBones[i];
					u32 boneIndex = 0;

					// see if we have already added this bone
					auto iter = model.mBoneIndexMap.find(aiBone->mName.C_Str());
					if(iter != model.mBoneIndexMap.end())
					{
						boneIndex = iter->second;
					}
					else
					{
						boneIndex = model.mBones.size();

						Bone* newBone = new Bone();
						ASSERT(aiBone->mName.length > 0, "Bone %d doesn't have a name!", boneIndex);
						newBone->name = aiBone->mName.C_Str();
						newBone->index = boneIndex;
						newBone->offsetTransform = *(Math::Matrix*)&aiBone->mOffsetMatrix;

						model.mBones.push_back(newBone);
						model.mBoneIndexMap.insert(std::make_pair(aiBone->mName.C_Str(), boneIndex));
					}

					for(u32 j = 0; j < aiBone->mNumWeights; ++j)
					{
						const aiVertexWeight& aiVertexWeight = aiBone->mWeights[j];
						BoneWeight weight;
						weight.boneIndex = boneIndex;
						weight.weight = aiVertexWeight.mWeight;
						vertexWeights[aiVertexWeight.mVertexId].push_back(weight);
					}
				}
			}
		}
	}

	// Read material data
	if(scene->HasMaterials())
	{
		for(u32 i = 0; i < scene->mNumMaterials; ++i)
		{
			aiMaterial* material = scene->mMaterials[i];

			const u32 textureCount = material->GetTextureCount(aiTextureType_DIFFUSE);
			for(u32 j = 0; j < textureCount; ++j)
			{
				aiString texturePath;
				if(material->GetTexture(aiTextureType_DIFFUSE, j, &texturePath) == AI_SUCCESS)
				{
					fprintf_s(pFile, texturePath.C_Str());
					fprintf_s(pFile, "\n");
				}
			}
		}
	}

	// Read animation data
	if(scene->HasAnimations())
	{
		model.mRoot = BuildSkeleton(*scene->mRootNode, model, nullptr);
		
		fprintf_s(pFile, "NumBones: %d\n", model.mBones.size());
		fprintf_s(pFile, "NumAnimations: %d\n", scene->mNumAnimations);

		for(u32 animIndex = 0; animIndex < scene->mNumAnimations; ++animIndex)
		{
			aiAnimation* aiAnimation = scene->mAnimations[animIndex];
			AnimationClip* animClip = new AnimationClip();

			animClip->mName = aiAnimation->mName.C_Str();
			animClip->mDuration = (f32)aiAnimation->mDuration;
			animClip->mTicksPerSecond = (f32)aiAnimation->mTicksPerSecond;
			if(animClip->mTicksPerSecond == 0.0f)
			{
				animClip->mTicksPerSecond = 1.0f;
			}

			fprintf_s(pFile, "%s\n", animClip->mName.c_str());
			fprintf_s(pFile, "%f\n", animClip->mDuration);
			fprintf_s(pFile, "%f\n", animClip->mTicksPerSecond);

			fprintf_s(pFile, "NumChannels: %d\n", aiAnimation->mNumChannels);
			for(u32 boneAnimIndex = 0; boneAnimIndex < aiAnimation->mNumChannels; ++boneAnimIndex)
			{
				aiNodeAnim* aiNodeAnim = aiAnimation->mChannels[boneAnimIndex];
				BoneAnimation* boneAnim = new BoneAnimation();

				boneAnim->mBoneIndex = model.mBoneIndexMap.at(aiNodeAnim->mNodeName.C_Str());
				fprintf_s(pFile, "%d\n", boneAnim->mBoneIndex);

				ASSERT(aiNodeAnim->mNumPositionKeys == aiNodeAnim->mNumRotationKeys, "Mismatched key count.");
				ASSERT(aiNodeAnim->mNumPositionKeys == aiNodeAnim->mNumScalingKeys, "Mismatched key count.");

				fprintf_s(pFile, "NumPositionKeys: %d\n", aiNodeAnim->mNumPositionKeys);
				for(u32 keyIndex = 0; keyIndex < aiNodeAnim->mNumPositionKeys; ++keyIndex)
				{
					const aiVectorKey& posKey = aiNodeAnim->mPositionKeys[keyIndex];
					const aiQuatKey& rotKey = aiNodeAnim->mRotationKeys[keyIndex];
					const aiVectorKey& scaleKey = aiNodeAnim->mScalingKeys[keyIndex];

					ASSERT(posKey.mTime == rotKey.mTime, "Mismatched key time");
					ASSERT(posKey.mTime == scaleKey.mTime, "Mismatched key time");

					Keyframe* keyframe = new Keyframe();
					keyframe->mTranslation = Math::Vector3(posKey.mValue.x, posKey.mValue.y, posKey.mValue.z);
					keyframe->mRotation = Math::Quaternion(rotKey.mValue.x, rotKey.mValue.y, rotKey.mValue.z, rotKey.mValue.w);
					keyframe->mScale = Math::Vector3(scaleKey.mValue.x, scaleKey.mValue.y, scaleKey.mValue.z);
					keyframe->mTime = (f32)posKey.mTime;

					fprintf_s(pFile, "%f %f %f\n", posKey.mValue.x, posKey.mValue.y, posKey.mValue.z);
					fprintf_s(pFile, "%f %f %f %f\n", rotKey.mValue.x, rotKey.mValue.y, rotKey.mValue.z, rotKey.mValue.w);
					fprintf_s(pFile, "%f %f %f\n", scaleKey.mValue.x, scaleKey.mValue.y, scaleKey.mValue.z);
					fprintf_s(pFile, "%f\n", (f32)posKey.mTime);

					boneAnim->mKeyframes.push_back(keyframe);
				}

				animClip->mBoneAnimations.push_back(boneAnim);
			}

			model.mAnimations.push_back(animClip);
		}
	}


	for(u32 boneIndex = 0; boneIndex < model.mBones.size(); ++boneIndex)
	{
		Bone* bone = model.mBones[boneIndex];

		//		-> name
		fprintf_s(pFile, "BoneName: %s", bone->name.c_str());
		fprintf_s(pFile, "\n");

		//		-> parent index
		fprintf_s(pFile, "Parent: %d\n", bone->parentIndex);

		//		-> children indices
		u16 size = bone->children.size();
		fprintf_s(pFile, "NumberOfChildren: %d\n", size);
		for(u16 i = 0; i < size; ++i)
		{
			fprintf_s(pFile, "%d ", bone->children[i]->index);
			if(i == size - 1)
			{
				fprintf_s(pFile, "\n");
			}
		}

		//		-> transform
		fprintf_s(pFile, "Transform: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f \n", 
				  bone->transform._11, bone->transform._12, bone->transform._13, bone->transform._14, 
				  bone->transform._21, bone->transform._22, bone->transform._23, bone->transform._24, 
				  bone->transform._31, bone->transform._32, bone->transform._33, bone->transform._34, 
				  bone->transform._41, bone->transform._42, bone->transform._43, bone->transform._44); 
		//		-> offset transform
		fprintf_s(pFile, "OffsetTransform: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f \n", 
				  bone->offsetTransform._11, bone->offsetTransform._12, bone->offsetTransform._13, bone->offsetTransform._14, 
				  bone->offsetTransform._21, bone->offsetTransform._22, bone->offsetTransform._23, bone->offsetTransform._24, 
				  bone->offsetTransform._31, bone->offsetTransform._32, bone->offsetTransform._33, bone->offsetTransform._34, 
				  bone->offsetTransform._41, bone->offsetTransform._42, bone->offsetTransform._43, bone->offsetTransform._44); 

		fprintf_s(pFile, "\n");
	}

	for(u32 meshIndex = 0; meshIndex < model.mMeshes.size(); ++meshIndex)
	{
		Mesh* mesh = model.mMeshes[meshIndex];
		const VertexWeights& vertexWeights = mesh->GetVertexWeights();
		u32 numVertexWeights = vertexWeights.size();
		fprintf_s(pFile, "NumVertexWeights: %d\n", numVertexWeights);

		for(u32 weightIndex = 0; weightIndex < numVertexWeights; ++weightIndex)
		{
			fprintf_s(pFile, "%d ", vertexWeights[weightIndex].size());
			for(u32 bIndex = 0; bIndex < vertexWeights[weightIndex].size(); ++bIndex)
			{
				fprintf_s(pFile, "%d %f ", vertexWeights[weightIndex][bIndex].boneIndex, vertexWeights[weightIndex][bIndex].weight);
			}
			fprintf_s(pFile, "\n");
		}
	}


	fclose(pFile);

	
}
示例#3
0
void ExportMd5Mesh(const CSkeletalMesh *Mesh)
{
	guard(ExportMd5Mesh);

	int i;

	UObject *OriginalMesh = Mesh->OriginalMesh;
	if (!Mesh->Lods.Num())
	{
		appNotify("Mesh %s has 0 lods", OriginalMesh->Name);
		return;
	}

	FArchive *Ar = CreateExportArchive(OriginalMesh, "%s.md5mesh", OriginalMesh->Name);
	if (!Ar) return;

	const CSkelMeshLod &Lod = Mesh->Lods[0];

	Ar->Printf(
		"MD5Version 10\n"
		"commandline \"Created with UE Viewer\"\n"
		"\n"
		"numJoints %d\n"
		"numMeshes %d\n"
		"\n",
		Mesh->RefSkeleton.Num(),
		Lod.Sections.Num()
	);

	// compute skeleton
	TArray<CCoords> BoneCoords;
	BuildSkeleton(BoneCoords, Mesh->RefSkeleton);

	// write joints
	Ar->Printf("joints {\n");
	for (i = 0; i < Mesh->RefSkeleton.Num(); i++)
	{
		const CSkelMeshBone &B = Mesh->RefSkeleton[i];

		const CCoords &BC = BoneCoords[i];
		CVec3 BP;
		CQuat BO;
		BP = BC.origin;
		BO.FromAxis(BC.axis);
		if (BO.w < 0) BO.Negate();				// W-component of quaternion will be removed ...

		Ar->Printf(
			"\t\"%s\"\t%d ( %f %f %f ) ( %.10f %.10f %.10f )\n",
			*B.Name, (i == 0) ? -1 : B.ParentIndex,
			VECTOR_ARG(BP),
			BO.x, BO.y, BO.z
		);
#if 0
	//!!
if (i == 32 || i == 34)
{
	CCoords BC;
	BC.origin = BP;
	BO.ToAxis(BC.axis);
	appNotify("Bone %d (%8.3f %8.3f %8.3f) - (%8.3f %8.3f %8.3f %8.3f)", i, VECTOR_ARG(BP), QUAT_ARG(BO));
#define C BC
	appNotify("INV   : o=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.origin ));
	appNotify("        0=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[0]));
	appNotify("        1=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[1]));
	appNotify("        2=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[2]));
#undef C
//	BO.Negate();
	BO.w *= -1;
	BO.ToAxis(BC.axis);
#define C BC
	appNotify("INV2  : o=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.origin ));
	appNotify("        0=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[0]));
	appNotify("        1=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[1]));
	appNotify("        2=%8.3f %8.3f %8.3f",    VECTOR_ARG(C.axis[2]));
#undef C
}
#endif
	}
	Ar->Printf("}\n\n");

	// collect weights information
	TArray<VertInfluences> Weights;				// Point -> Influences
	Weights.AddZeroed(Lod.NumVerts);
	for (i = 0; i < Lod.NumVerts; i++)
	{
		const CSkelMeshVertex &V = Lod.Verts[i];
		CVec4 UnpackedWeights;
		V.UnpackWeights(UnpackedWeights);
		for (int j = 0; j < NUM_INFLUENCES; j++)
		{
			if (V.Bone[j] < 0) break;
			VertInfluence *I = new (Weights[i].Inf) VertInfluence;
			I->Bone   = V.Bone[j];
			I->Weight = UnpackedWeights[j];
		}
	}

	CIndexBuffer::IndexAccessor_t Index = Lod.Indices.GetAccessor();

	// write meshes
	// we are using some terms here:
	// - "mesh vertex" is a vertex in Lod.Verts[] array, global for whole mesh
	// - "surcace vertex" is a vertex from the mesh stripped to only one (current) section
	for (int m = 0; m < Lod.Sections.Num(); m++)
	{
		const CMeshSection &Sec = Lod.Sections[m];

		TArray<int>  MeshVerts;					// surface vertex -> mesh vertex
		TArray<int>  BackWedge;					// mesh vertex -> surface vertex
		TArray<bool> UsedVerts;					// mesh vertex -> surface: used of not
		TArray<int>  MeshWeights;				// mesh vertex -> weight index
		MeshVerts.Empty(Lod.NumVerts);
		UsedVerts.AddZeroed(Lod.NumVerts);
		BackWedge.AddZeroed(Lod.NumVerts);
		MeshWeights.AddZeroed(Lod.NumVerts);

		// find verts and triangles for current material
		for (i = 0; i < Sec.NumFaces * 3; i++)
		{
			int idx = Index(i + Sec.FirstIndex);

			if (UsedVerts[idx]) continue;		// vertex is already used in previous triangle
			UsedVerts[idx] = true;
			int locWedge = MeshVerts.Add(idx);
			BackWedge[idx] = locWedge;
		}

		// find influences
		int WeightIndex = 0;
		for (i = 0; i < Lod.NumVerts; i++)
		{
			if (!UsedVerts[i]) continue;
			MeshWeights[i] = WeightIndex;
			WeightIndex += Weights[i].Inf.Num();
		}

		// mesh header
		const UUnrealMaterial *Tex = Sec.Material;
		if (Tex)
		{
			Ar->Printf(
				"mesh {\n"
				"\tshader \"%s\"\n\n",
				Tex->Name
			);
			ExportObject(Tex);
		}
		else
		{
			Ar->Printf(
				"mesh {\n"
				"\tshader \"material_%d\"\n\n",
				m
			);
		}
		// verts
		Ar->Printf("\tnumverts %d\n", MeshVerts.Num());
		for (i = 0; i < MeshVerts.Num(); i++)
		{
			int iPoint = MeshVerts[i];
			const CSkelMeshVertex &V = Lod.Verts[iPoint];
			Ar->Printf("\tvert %d ( %f %f ) %d %d\n",
				i, V.UV.U, V.UV.V, MeshWeights[iPoint], Weights[iPoint].Inf.Num());
		}
		// triangles
		Ar->Printf("\n\tnumtris %d\n", Sec.NumFaces);
		for (i = 0; i < Sec.NumFaces; i++)
		{
			Ar->Printf("\ttri %d", i);
#if MIRROR_MESH
			for (int j = 2; j >= 0; j--)
#else
			for (int j = 0; j < 3; j++)
#endif
				Ar->Printf(" %d", BackWedge[Index(Sec.FirstIndex + i * 3 + j)]);
			Ar->Printf("\n");
		}
		// weights
		Ar->Printf("\n\tnumweights %d\n", WeightIndex);
		int saveWeightIndex = WeightIndex;
		WeightIndex = 0;
		for (i = 0; i < Lod.NumVerts; i++)
		{
			if (!UsedVerts[i]) continue;
			for (int j = 0; j < Weights[i].Inf.Num(); j++)
			{
				const VertInfluence &I = Weights[i].Inf[j];
				CVec3 v;
				v = Lod.Verts[i].Position;
#if MIRROR_MESH
				v[1] *= -1;						// y
#endif
				BoneCoords[I.Bone].TransformPoint(v, v);
				Ar->Printf(
					"\tweight %d %d %f ( %f %f %f )\n",
					WeightIndex, I.Bone, I.Weight, VECTOR_ARG(v)
				);
				WeightIndex++;
			}
		}
		assert(saveWeightIndex == WeightIndex);

		// mesh footer
		Ar->Printf("}\n");
	}

	delete Ar;

	unguard;
}
示例#4
0
void USkelModel::Serialize(FArchive &Ar)
{
	guard(USkelModel::Serialize);

	assert(Ar.IsLoading);					// no saving ...
	Super::Serialize(Ar);

	// USkelModel data
	int nummeshes;
	int numjoints;
	int numframes;
	int numsequences;
	int numskins;
	int rootjoint;

	FVector					PosOffset;		// Offset of creature relative to base
	FRotator				RotOffset;		// Offset of creatures rotation

	TArray<RMesh>			meshes;
	TArray<RJoint>			joints;
	TArray<FRSkelAnimSeq>	AnimSeqs;		// Compressed animation data for sequence
	TArray<RAnimFrame>		frames;

	Ar << nummeshes << numjoints << numframes << numsequences << numskins << rootjoint;
	Ar << meshes << joints << AnimSeqs << frames << PosOffset << RotOffset;

	int modelIdx;
	// create all meshes first, then fill them (for better view order)
	for (modelIdx = 0; modelIdx < meshes.Num(); modelIdx++)
	{
		// create new USkeletalMesh
		// use "CreateClass()" instead of "new USkeletalMesh" to allow this object to be
		// placed in GObjObjects array and be browsable in a viewer
		USkeletalMesh *sm = static_cast<USkeletalMesh*>(CreateClass("SkeletalMesh"));
		char nameBuf[256];
		appSprintf(ARRAY_ARG(nameBuf), "%s_%d", Name, modelIdx);
		const char *name = appStrdupPool(nameBuf);
		Meshes.Add(sm);
		// setup UOnject
		sm->Name         = name;
		sm->Package      = Package;
		sm->PackageIndex = INDEX_NONE;		// not really exported
		sm->Outer        = NULL;
	}
	// create animation
	Anim = static_cast<UMeshAnimation*>(CreateClass("MeshAnimation"));
	Anim->Name         = Name;
	Anim->Package      = Package;
	Anim->PackageIndex = INDEX_NONE;		// not really exported
	Anim->Outer        = NULL;
	ConvertRuneAnimations(*Anim, joints, AnimSeqs);
	Anim->ConvertAnims();					//?? second conversion
	// get baseframe
	assert(strcmp(Anim->AnimSeqs[0].Name, "baseframe") == 0);
	const TArray<AnalogTrack> &BaseAnim = Anim->Moves[0].AnimTracks;
	// compute bone coordinates
	TArray<CCoords> BoneCoords;
	BuildSkeleton(BoneCoords, joints, BaseAnim);

	// setup meshes
	for (modelIdx = 0; modelIdx < meshes.Num(); modelIdx++)
	{
		int i, j;
		const RMesh  &src = meshes[modelIdx];
		USkeletalMesh *sm = Meshes[modelIdx];
		sm->Animation = Anim;
		// setup ULodMesh
		sm->RotOrigin = RotOffset;
		sm->MeshScale.Set(1, 1, 1);
		sm->MeshOrigin = PosOffset;
		// copy skeleton
		sm->RefSkeleton.Empty(joints.Num());
		for (i = 0; i < joints.Num(); i++)
		{
			const RJoint &J = joints[i];
			FMeshBone *B = new(sm->RefSkeleton) FMeshBone;
			B->Name = J.name;
			B->Flags = 0;
			B->ParentIndex = (J.parent > 0) ? J.parent : 0;		// -1 -> 0
			// copy bone orientations from base animation frame
			B->BonePos.Orientation = BaseAnim[i].KeyQuat[0];
			B->BonePos.Position    = BaseAnim[i].KeyPos[0];
		}
		// copy vertices
		int VertexCount = sm->VertexCount = src.verts.Num();
		sm->Points.Empty(VertexCount);
		for (i = 0; i < VertexCount; i++)
		{
			const RVertex &v1 = src.verts[i];
			FVector *V = new(sm->Points) FVector;
			// transform point from local bone space to model space
			BoneCoords[v1.joint1].UnTransformPoint(CVT(v1.point1), CVT(*V));
		}
		// copy triangles and create wedges
		// here we create 3 wedges for each triangle.
		// it is possible to reduce number of wedges by finding duplicates, but we don't
		// need it here ...
		int TrisCount = src.tris.Num();
		sm->Triangles.Empty(TrisCount);
		sm->Wedges.Empty(TrisCount * 3);
		int numMaterials = 0;		// should detect real material count
		for (i = 0; i < TrisCount; i++)
		{
			const RTriangle &tri = src.tris[i];
			// create triangle
			VTriangle *T = new(sm->Triangles) VTriangle;
			T->MatIndex = tri.polygroup;
			if (numMaterials <= tri.polygroup)
				numMaterials = tri.polygroup+1;
			// create wedges
			for (j = 0; j < 3; j++)
			{
				T->WedgeIndex[j] = sm->Wedges.Num();
				FMeshWedge *W = new(sm->Wedges) FMeshWedge;
				W->iVertex = tri.vIndex[j];
				W->TexUV   = tri.tex[j];
			}
			// reverse order of triangle vertices
			Exchange(T->WedgeIndex[0], T->WedgeIndex[1]);
		}
		// build influences
		for (i = 0; i < VertexCount; i++)
		{
			const RVertex &v1 = src.verts[i];
			FVertInfluence *Inf = new(sm->VertInfluences) FVertInfluence;
			Inf->PointIndex = i;
			Inf->BoneIndex  = v1.joint1;
			Inf->Weight     = v1.weight1;
			if (Inf->Weight != 1.0f)
			{
				// influence for 2nd bone
				Inf = new(sm->VertInfluences) FVertInfluence;
				Inf->PointIndex = i;
				Inf->BoneIndex  = v1.joint2;
				Inf->Weight     = 1.0f - v1.weight1;
			}
		}
		// create materials
		for (i = 0; i < numMaterials; i++)
		{
			const char *texName = src.PolyGroupSkinNames[i];
			FMeshMaterial *M1 = new(sm->Materials) FMeshMaterial;
			M1->PolyFlags    = src.GroupFlags[i];
			M1->TextureIndex = sm->Textures.Num();
			if (strcmp(texName, "None") == 0)
			{
				// texture should be set from script
				sm->Textures.Add(NULL);
				continue;
			}
			// find texture in object's package
			int texExportIdx = Package->FindExport(texName);
			if (texExportIdx == INDEX_NONE)
			{
				appPrintf("ERROR: unable to find export \"%s\" for mesh \"%s\" (%d)\n",
					texName, Name, modelIdx);
				continue;
			}
			// load and remember texture
			UMaterial *Tex = static_cast<UMaterial*>(Package->CreateExport(texExportIdx));
			sm->Textures.Add(Tex);
		}
		// setup UPrimitive properties using 1st animation frame
		// note: this->BoundingBox and this->BoundingSphere are null
		const RAnimFrame &F = frames[0];
		assert(strcmp(AnimSeqs[0].Name, "baseframe") == 0 && AnimSeqs[0].StartFrame == 0);
		CVec3 mins, maxs;
		sm->BoundingBox = F.bounds;
		mins = CVT(F.bounds.Min);
		maxs = CVT(F.bounds.Max);
		CVec3 &center = CVT(sm->BoundingSphere);
		for (i = 0; i < 3; i++)
			center[i] = (mins[i] + maxs[i]) / 2;
		sm->BoundingSphere.R = VectorDistance(center, mins);

		// create CSkeletalMesh
		sm->ConvertMesh();
	}

	unguard;
}