Bullet 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_x;
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;
}
デモプログラム
今回説明した内容に対応する処理は、zg_sbskin.cppに書いてあります(上記のソースコード)。
※128式ミクダヨーさん Ver. 2.01は含まれません
リンクなど
この記事が役に立ったという方は、サポートお願いします。今後の製作の励みになります。