見出し画像

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は含まれません

リンクなど


この記事が役に立ったという方は、サポートお願いします。今後の製作の励みになります。