Bulletでぬいぐるみを作ってみる Part1 簡易形状によるスキニング

SoftBodyを使って、ぬいぐるみを再現しみます。SoftBodyのポリゴン数に制限があるようなので、ローポリモデルをソフトボディーで動かし、その動きをハイポリモデルに反映するという方法を採用します。これでSoftBodyの制限回避と計算負荷を軽減しつつ、見た目の良いハイポリモデルを動かすことが可能になります。
今回、解説動画を作成しました。大まかな説明は動画で行って、ここではローポリとハイポリモデルの連動について解説します。

SoftBodyぬいぐるみ
SoftBodyぬいぐるみ


解説動画

【ニコニコ動画】物理エンジンでぬいぐるみを作ってみた


簡易ポリゴンモデルと複雑なモデルの連動

SoftBodyはポリゴン数に制限があるようです。ためしに約2万ポリゴンを登録すると動作しませんでした。それと計算負荷の問題もあるので、ポリゴン数を少なく抑える必要があります。ゲームなどで使うとき、見た目の良いハイポリモデルを使用できません。そこで、SoftBodyで簡易モデル(ローポリモデル)を動かし、その動きを複雑なモデル(ハイポリモデル)に反映させることで解決します。

基本的な仕組み

ローポリモデル(SoftBody)を構成する三角形ポリゴンとハイポリモデルの頂点(複数)を対応付け、ローポリモデルの動きをそれぞれ対応する頂点に適応させます。
これは
「ボーン」=「ローポリモデルの三角形ポリゴン」
「スキン」=「ハイポリモデル」
とした、ボーンによるスキン変形と同じ処理になります。
ボーン数の制限に注意が必要ですが、既存のグラフィック処理をそのまま使用できます。(DX11では1つの定数バッファに約1300ボーン)

対応付け=ボーンウエイト付け

3Dモデラーなどではウエイト付けができない(もしかしたらあるかも)ため、プログラムで自動的に割り振り。ローポリモデルのポリゴンとハイポリモデルの頂点の距離を計算し、近いものを対応付け、ウエイトは距離に反比例。詳しくは下記のソースコードを(関数AssignTriangle)。

SoftBodyから三角形ポリゴン取得

SoftBodyは三角形ポリゴンで構成(三角錐もあるが今回未使用)されているため、その情報を取得するのみ。三角形ポリゴンから姿勢行列が求められるため、初期姿勢行列との差分をボーンによるスキン変形処理へ(ソースコード関数GetSoftBodySkinningPose)。


ソースコード

一部モデルデータ取得設定関連の処理がありますが、このソースコードを見れば、おおまか処理の流れや、ウエイト付け計算が理解できると思います。

struct TRIANGLE{
	XMVECTOR p[3];
	XMVECTOR n;//法線 |n|=1
	XMVECTOR min,max;
};

XMVECTOR TriangleNormal(XMVECTOR p0, XMVECTOR p1, XMVECTOR p2)
{
	XMVECTOR v10 = XMVectorSubtract(p1, p0);
	XMVECTOR v20 = XMVectorSubtract(p2, p0);
	XMVECTOR nor = XMVector3Cross(v10, v20);
	return XMVector3Normalize(nor);
}

XMMATRIX TrianglePose(const TRIANGLE& tri)
{
	XMMATRIX m;
	// 重心を原点
	XMVECTOR pos = XMVectorScale(XMVectorAdd(XMVectorAdd(tri.p[0],tri.p[1]),tri.p[2]),1.0f/3.0f);
	// 法線=Z軸
	XMVECTOR z = XMVector3Normalize(tri.n);
	// Y 重心からp[0]の方向
	XMVECTOR y = XMVector3Normalize(XMVectorSubtract(tri.p[0],pos));
	// X y×z
	XMVECTOR x = XMVector3Normalize(XMVector3Cross(y,z));

	m.r[0] = XMVectorSetW(x,0.0f);
	m.r[1] = XMVectorSetW(y,0.0f);
	m.r[2] = XMVectorSetW(z,0.0f);
	m.r[3] = XMVectorSetW(pos,1.0f);
	return m;
}


// 点と三角形の最短距離
FLOAT Distance(const TRIANGLE& tri, XMVECTOR p)
{
	// 三角形の平面との垂直距離と点
	XMVECTOR dplane = XMVector3Dot(XMVectorSubtract(tri.p[0],p), tri.n);
	XMVECTOR pplane = XMVectorSubtract(p, XMVectorMultiply(tri.n,dplane));//dplane.xyzwに距離なのでMul
	
	//pplaneが三角形の内部なら最短距離
	for(int i=0;i<3;++i){
		int i0 = i;
		int i1 = (i+1)%3;
		XMVECTOR p10 = XMVectorSubtract(tri.p[i1],tri.p[i0]);	//三角形の辺
		XMVECTOR pp0 = XMVectorSubtract(pplane, tri.p[i0]);	//
		XMVECTOR cs0 = XMVector3Cross(p10,pp0);
		XMVECTOR in0 = XMVector3Dot(tri.n,cs0);
		if(XMVectorGetX(in0) < 0.0f){//三角形の外
			// 辺p[i1]-p[i0]との最短距離
			XMVECTOR p0 = XMVectorSubtract(p,tri.p[i0]);
			XMVECTOR p10n = XMVector3Normalize(p10);
			XMVECTOR d10 = XMVector3Length(p10);
			XMVECTOR l0 = XMVector3Dot(p10n,p0);
			if(XMVectorGetX(l0) < 0.0f){//三角形の点p[i0]より外側
				//点p[i0]との距離が最短距離
				XMVECTOR d = XMVectorSubtract(tri.p[i0],p);
				return XMVectorGetX(XMVector3Length(d));
			}
			if(XMVectorGetX(l0) > XMVectorGetX(d10)){//三角形の点p[i1]より外側
				//点p[i1]との距離が最短距離
				XMVECTOR d = XMVectorSubtract(tri.p[i1],p);
				return XMVectorGetX(XMVector3Length(d));
			}
			XMVECTOR ph = XMVectorAdd(XMVectorMultiply(p10n,l0), tri.p[i0]);
			XMVECTOR d = XMVectorSubtract(p,ph);
			return XMVectorGetX(XMVector3Length(d));
		}
	}

	// pplaneが三角形の内部
	return XMVectorGetX(XMVectorAbs(dplane));
}
//-----------------------------------
bool AssignTriangle(const TRIANGLE* tri,UINT trinum, render::Geometry& geom, render::Mesh& mesh)
{
	using render::VertexBuff;
	using render::VertexDecl;
	U32 bidx_slot,bwgt_slot,pos_slot;
	if(!geom.GetSlot(render::VA_POSITION,pos_slot))return false;

	// ボーン変形がない場合作成
	if(!geom.GetSlot(render::VA_BONEINDEX,bidx_slot)){
		bidx_slot = geom.AddVertex();
		VertexBuff* bi = geom.GetVertex(bidx_slot);
		if(!bi)return false;
		if(!bi->Create<UVECTOR4>(render::VA_BONEINDEX, geom.VertexCount()))return false;
		VertexDecl* decl = mesh.GetDecl(mesh.AddDecl());
		if(decl){
			decl->uSlot = bidx_slot;
			decl->uStream = 0;
			decl->idxSema = 0;
			decl->strSema = "BONEINDEX";
		}
	}
	if(!geom.GetSlot(render::VA_BONEWEIGHT,bwgt_slot)){
		bwgt_slot = geom.AddVertex();
		VertexBuff* bw = geom.GetVertex(bwgt_slot);
		if(!bw)return false;
		if(!bw->Create<UVECTOR4>(render::VA_BONEWEIGHT, geom.VertexCount()))return false;
		VertexDecl* decl = mesh.GetDecl(mesh.AddDecl());
		if(decl){
			decl->uSlot = bwgt_slot;
			decl->uStream = 0;
			decl->idxSema = 0;
			decl->strSema = "BONEWEIGHT";
		}
	}

	VertexBuff* bidx = geom.GetVertex(bidx_slot);
	VertexBuff* bwgt = geom.GetVertex(bwgt_slot);
	VertexBuff* pos = geom.GetVertex(pos_slot);
	if(!bidx || !bwgt)return false;

	FVECTOR3* vp;
	UVECTOR4* vbi,*vbw;
	U32 vbi_num, vbw_num,vp_num;
	if(!pos->GetBuff(vp,vp_num))return false;
	if(!bidx->GetBuff(vbi,vbi_num))return false;
	if(!bwgt->GetBuff(vbw,vbw_num))return false;
	if(vp_num != vbi_num || vp_num != vbw_num)return false;

	for(U32 i=0;i<vp_num;++i){
		auto& p = vp[i];
		XMVECTOR xp = XMVectorSet(p.x,p.y,p.z,1.0f);
		FLOAT min_dist[4] = {0,0,0,0};
		U32 min_tri[4] = {trinum,trinum,trinum,trinum};
		FLOAT far_dist = (std::numeric_limits<FLOAT>::max)();

		for(U32 t=0;t<trinum;++t){
			auto& tt = tri[t];
			//BBoxで事前チェック
			XMVECTOR mx = XMVectorSubtract(xp, tt.max);
			if( XMVectorGetX(mx) > far_dist)continue;
			if( XMVectorGetY(mx) > far_dist)continue;
			if( XMVectorGetZ(mx) > far_dist)continue;

			XMVECTOR mn = XMVectorSubtract(tt.min, xp);
			if( XMVectorGetX(mn) > far_dist)continue;
			if( XMVectorGetY(mn) > far_dist)continue;
			if( XMVectorGetZ(mn) > far_dist)continue;

			FLOAT dist = Distance(tt,xp);
			if( dist > far_dist ){
				continue;//遠すぎ
			}
			for(U32 s=0;s<4;++s){
				if(min_tri[s]==trinum){
					// 新規追加
					min_tri[s] = t;
					min_dist[s] = dist;
					if(s==3)far_dist = dist;
					break;
				}
				if(dist > min_dist[s])continue;//次

				// 心太
				for(U32 j=3;j>s;--j){
					min_dist[j] = min_dist[j-1];
					min_tri[j] = min_tri[j-1];
				}

				min_dist[s] = dist;
				min_tri[s] = t;
				if(s==3){far_dist = dist;}
				break;
			}
		}

		//三角形に対する重み付 0~100(%)
		if(min_tri[0] == trinum){
			//なし
			vbw[i] = UVECTOR4(100,0,0,0);
			vbi[i] = UVECTOR4(0,0,0,0);
			continue;
		}
		if(min_dist[0] < 0.00001f){
			//距離0
			vbw[i] = UVECTOR4(100,0,0,0);
			vbi[i] = UVECTOR4(min_tri[0],0,0,0);
			continue;
		}

		// 最短距離との割合を重みに
		FLOAT wgt_sum = 1.0f;
		for(U32 b=1;b<4;++b){
			if(min_tri[b] >= trinum)break;
			// 遠すぎるものは無効 とりあえず最短より3倍
			if( min_dist[b]/min_dist[0] > 3.0f){//10
				min_dist[b] = 0.0f;
				min_tri[b] = trinum;
			}else{
				wgt_sum += min_dist[0]/min_dist[b];
			}
		}

		U32 wgt_isum = 0;
		for(U32 b=0;b<4;++b){
			vbi[i].v[b] = min_tri[b];
			if(min_tri[b]==trinum){
				vbw[i].v[b] = 0;
				vbi[i].v[b] = 0;
			}else{
				vbw[i].v[b] = (U32)(100.0f*(min_dist[0]/min_dist[b]/wgt_sum)+0.00001f);
			}
			wgt_isum += vbw[i].v[b];
		}
		if( wgt_isum < 100-4){
			//計算がおかしい 
		}

		//誤差補正
		if( wgt_isum < 100 && vbw[i].v[0]>0){
			vbw[i].v[0] += 100-wgt_isum;
		}
		if( wgt_isum > 100 && vbw[i].v[0]>0){
			//ありえないけど
			vbw[i].v[0] -= wgt_isum-100;
			if(vbw[i].v[0]>100)vbw[i].v[0]=100;
		}
	}
	return true;
}
//-----------------------------------
// ポリゴンメッシュの頂点にSoftBodyのフェイス(三角形)を割り当てる
bool AssignTriangle(const TRIANGLE* tri,UINT num, zg::render::Model& model)
{
	for(U32 o=0;o<model.ObjNum();++o){
		render::Object* obj = model.GetObj(o);
		if(!obj)return false;
		for(U32 m=0;m<obj->MeshNum();++m){
			render::Mesh* mesh = obj->GetMesh(m);
			if(!mesh)return false;
			render::Geometry* geom = model.GetGeom(mesh->getGeomIdx());
			if(!geom)continue;
			AssignTriangle(tri, num, *geom, *mesh);
		}
	}
	return true;
}

}

//--------------------------------------------
bool CreateSoftBodySkinning(const SBGeometry& geom, const SBConfig& cfg, zg::SPtr<zg::render::Model> view_model)
{
	if(!view_model)return false;

	const std::vector<btScalar>& vertex = geom.aVertex;
	const std::vector<int>& index = geom.aIndex;

	zg::render::Model& model = *view_model;

	// ポリゴン情報取得用
	btSoftBodyWorldInfo wi;//いいのか? ワールドに追加しないので
	std::unique_ptr<btSoftBody> psb;
	psb.reset(CreateSoftBody(geom,cfg, &wi));
	if(!psb)return false;
	
	// SoftBodyの面取得
	zg::BinObject triangle;

	auto& faces = psb->m_faces;
	UINT polynum = faces.size();
	triangle.Create(polynum*sizeof(TRIANGLE),16);

	// とりあえず
	// 通常のボーンスキニングの機能を間借り
	model.ResizeBone(polynum);

	for(UINT fi=0;fi<polynum;++fi){
		const auto& f = faces[fi];
		TRIANGLE& tri = triangle.get<TRIANGLE*>()[fi];
		for(UINT i=0;i<3;++i){
			const auto& p = f.m_n[i]->m_q;
			const auto& n = f.m_n[i]->m_n;
			tri.p[i] = XMVectorSet(p.getX(), p.getY(),p.getZ(),1);
		}
		tri.n = TriangleNormal(tri.p[0],tri.p[1],tri.p[2]);
		tri.min = XMVectorMin(XMVectorMin(tri.p[0],tri.p[1]),tri.p[2]);
		tri.max = XMVectorMax(XMVectorMax(tri.p[0],tri.p[1]),tri.p[2]);
			
		// 三角形の姿勢行列を求める(初期姿勢)
		render::Bone* bone = model.GetBone(fi);
		if(bone){
			// 三角形の姿勢行列
			bone->mtxPose = dx11::XMToFM(TrianglePose(tri));
		}
	}

	//メッシュの頂点への割り当て 三角形=ボーンとしてスキニングを行う
	AssignTriangle(triangle.get<TRIANGLE*>(), polynum, model);
		

	return true;
}

//--------------------------------------------
bool GetSoftBodySkinningPose(btSoftBody* sb, DirectX::XMMATRIX* ary, zg::U32 ary_num)
{
	if( !sb || !ary){
		return false;
	}

	auto& faces = sb->m_faces;
	U32 polynum = faces.size();

	for(UINT fi=0;fi<polynum;++fi){
		const auto& f = faces[fi];
		TRIANGLE tri;
		for(UINT i=0;i<3;++i){
			const auto& p = f.m_n[i]->m_q;
			tri.p[i] = XMVectorSet(p.getX(), p.getY(),p.getZ(),1);
		}
		tri.n = TriangleNormal(tri.p[0],tri.p[1],tri.p[2]);
			
		// 三角形の姿勢行列
		ary[fi] = TrianglePose(tri);
	}
	return true;
}

(追記2014/6/1) SoftBodyのノード(頂点)位置取得にm_qを使っていますが、正しくはm_xです。サンプルの更新はしていません。


サンプルプログラム

解説動画を作るためのプログラムが大半を占めますが、今回説明した内容に対応する処理は、zg_sbskin.cppに書いてあります(上記のソースコード)。
※128式ミクダヨーさん Ver. 1.70はサンプルプログラムには含まれません

“BulletのSoftBodyでスカートを揺らしてみる” をダウンロード BulletSoftBody2.zip – 1285 回のダウンロード – 955 KB


リンクなど

Bullet Physics Library

http://bulletphysics.org/

128式ミクダヨーさん Ver. 1.70

【ニコニコ動画】【MMD】知られざるダヨー 【モデル配布】

PMXモデルデータ仕様

「PMXエディタ」 / とある工房

3Dモデリングソフトウェア「Metasequoia 4」

http://metaseq.net/jp/

動画制作ツール

【ニコニコ動画】ゆっくり実況がすごく簡単に作れるようになった【ゆっくりMovieMaker3】

動画BGM

【ニコニコ動画】PCエンジンで TOWN(アイドルマスター)を演奏してってってー

「Bulletでぬいぐるみを作ってみる Part1 簡易形状によるスキニング」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.