cvl-robot's diary

研究ノート メモメモ https://github.com/dotchang/

動力学シミュレータofxBulletを静的な衝突判定ライブラリとしてだけ使う方法

OMPLが自由に使えるようになって経路計画がお手軽で楽しいのですが、衝突判定を自分で書くのが億劫です。プリミティブな形状同士の衝突判定は一つ一つであればたいして難しくもありませんが、効率的に、さまざまな形状の、複数物体同士を、衝突するものとしないもののグループを考慮して、衝突判定する、なんていうことを考え始めると、途端に面倒くさくなります。

衝突判定ライブラリ探し

衝突判定ライブラリはすでに様々な提案があるのですが、openFrameworksで簡単に使えるということを考えるとC++がよく、なおかつ誰かがすでにaddonを作ってくれていることが望ましいということになります。
文献1に素晴らしいまとめがあります。どの程度最新のものまでをカバーしているのかわかりませんが、少なくともこの2年ぐらいのメジャーなライブラリが表にまとめられています。この中で、該当しそうなものを探すとFCLかBullet, ODE, ReactPhysics3dが良さそうだとわかります。FCLのofAddonを探してみましたが見つかりませんでした。またFCLはまだ開発途上だと本家のページに注意書きがあります。ofxODEもよさそうですが、以前使った感触として分かりにくかった記憶があるので今回は避けます。ofxBulletは動力学シミュレーションを使った派手な演出を行うために、結構有名です。動力学計算の機能は不要ですが、今回はofxBulletの衝突判定機能だけを取り出して使ってみたいと思います。

衝突判定のサンプル

物体が動かない時間が止まった状態で、こちらで与えた物体の位置姿勢で、物体同士が衝突を起こしているかどうかを判別したいと思います。サンプルとして、球、箱、円柱の3種類のプリミティブに適当な位置姿勢を与えて判定することにします。ただし、球と箱は仲がよいもの同士と仮定して、球と箱は衝突状態でも衝突を判定しないことにします。

物体の位置姿勢を強制的に更新する方法は[4]で示されています。

衝突フィルタリングの方法は、[5][6]で示されています。
worldへの登録は、shapes->add関数ではなく、フラグを付けつつ自前でやります。world.update()は静的に判定できれば良いので、時間を0、更新回数を1回にします。

大雑把なフローチャートはこんな感じで、簡単。衝突判定結果は、コールバック関数の中でフラグの更新をして返されます。
f:id:cvl-robot:20180524122228p:plain

ofApp.h

#pragma once

#include "ofMain.h"
#include "ofxBullet.h"

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		
		ofxBulletWorldRigid	world;
		std::vector<ofxBulletRigidBody*> shapes;
		ofEasyCam cam;
		ofLight light;
		std::vector<bool> bColliding;

		void onCollision(ofxBulletCollisionData& cdata);
};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::onCollision(ofxBulletCollisionData& cdata) {
	for (int i = 0; i < shapes.size(); i++) {
		if(*shapes[i] == cdata) {
			bColliding[i] = true;
		}
	}
}

//--------------------------------------------------------------
void ofApp::setup(){
	ofSetFrameRate(60);
	ofSetVerticalSync(true);
	ofSetBackgroundColor(ofColor::black);

	world.setup();
	world.enableCollisionEvents();
	ofAddListener(world.COLLISION_EVENT, this, &ofApp::onCollision);
	world.setCamera(&cam);
	world.setGravity(ofVec3f(0, 0, -9.8f));

	// Collision Settings
#define BIT(x) (1<<(x))
	enum collisiontypes {
		COL_NOTHING = 0,
		COL_SPHERE = BIT(0),
		COL_BOX = BIT(1),
		COL_CAPSULE = BIT(2),
		COL_CONE = BIT(3),
		COL_CYLINDER = BIT(4)
	};

	// Objects
	shapes.push_back(new ofxBulletSphere());
	((ofxBulletSphere*)shapes.back())->init(ofBtGetSphereCollisionShape(0.8f));
	((ofxBulletSphere*)shapes.back())->create(world.world);
	shapes.back()->setActivationState(DISABLE_DEACTIVATION);
	world.world->addCollisionObject(shapes.back()->getCollisionObject(), COL_SPHERE, COL_CAPSULE | COL_CONE | COL_CYLINDER);

	shapes.push_back(new ofxBulletBox());
	((ofxBulletBox*)shapes.back())->init(ofBtGetBoxCollisionShape(1.2f));
	((ofxBulletBox*)shapes.back())->create(world.world);
	shapes.back()->setActivationState(DISABLE_DEACTIVATION);
	world.world->addCollisionObject(shapes.back()->getCollisionObject(), COL_BOX, COL_CAPSULE | COL_CONE | COL_CYLINDER);

#if 0
	shapes.push_back(new ofxBulletCapsule());
	((ofxBulletCapsule*)shapes.back())->init(ofBtGetCapsuleCollisionShape(0.4f, 2.2f));
	((ofxBulletCapsule*)shapes.back())->create(world.world, ofVec3f(0.f, 0.f, 0.f), 0.2f);
	shapes.back()->setActivationState(DISABLE_DEACTIVATION);
	world.world->addCollisionObject(shapes.back()->getCollisionObject(), COL_CAPSULE, COL_SPHERE | COL_BOX | COL_CONE | COL_CYLINDER);

	shapes.push_back(new ofxBulletCone());
	((ofxBulletCone*)shapes.back())->init(ofBtGetConeCollisionShape(0.9f, 0.6f));
	((ofxBulletCone*)shapes.back())->create(world.world);
	shapes.back()->setActivationState(DISABLE_DEACTIVATION);
	world.world->addCollisionObject(shapes.back()->getCollisionObject(), COL_CONE, COL_SPHERE | COL_BOX | COL_CAPSULE);
#endif

	shapes.push_back(new ofxBulletCylinder());
	((ofxBulletCylinder*)shapes.back())->init(ofBtGetCylinderCollisionShape(0.7f, 0.7f));
	((ofxBulletCylinder*)shapes.back())->create(world.world, ofVec3f(0.f, 0.f, 0.f), 0.4f);
	shapes.back()->setActivationState(DISABLE_DEACTIVATION);
	world.world->addCollisionObject(shapes.back()->getCollisionObject(), COL_CYLINDER, COL_SPHERE | COL_BOX | COL_CAPSULE);

	for (size_t i(0); i < shapes.size(); ++i) {
		bColliding.push_back(false);
	}

	// Camera and Light settings
	light.setParent(cam, true);
	cam.setFarClip(16.f);
	cam.setNearClip(0.001f);
	cam.setDistance(8.f);
	cam.setPosition(0.f, -8.f, 0.f);
	cam.lookAt(ofVec3f(0.f, 0.f, 0.f), ofVec3f(0.f, 0.f, 1.f));
}

//--------------------------------------------------------------
void ofApp::update(){
	for (size_t i(0); i < bColliding.size(); i++) {
		bColliding[i] = false;
	}
	world.update(0.f, 1);
}

//--------------------------------------------------------------
void ofApp::draw(){
	ofEnableDepthTest();
	ofEnableLighting();
	light.enable();

	cam.begin();
	ofSetColor(ofColor::white);
	for (size_t i(0); i < shapes.size(); i++) {
		ofPushStyle();
		if (bColliding[i]) ofSetColor(ofColor::yellow);
		shapes[i]->draw();
		ofPopStyle();
	}

	// draw GroundPlane
	ofDisableLighting();
	ofPushMatrix();
	ofTranslate(0.f, 0.f, 0.f);
	ofRotate(90.f, 0.f, 1.f, 0.f);
	ofPushStyle();
	ofSetColor(ofColor::blue);
	ofDrawGridPlane(0.1f, 10, false);
	ofPopStyle();
	ofPopMatrix();

	cam.end();
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key){
	// Updates for Shapes Position
	for (size_t i(0); i < shapes.size(); i++) {
		ofVec3f pos = ofVec3f(ofRandom(-2.f, 2.f), ofRandom(-2.f, 2.f), ofRandom(-2.f, 2.f));
		ofQuaternion quat;
		quat.makeRotate(ofVec3f(0.f, 1.f, 0.f), ofVec3f(ofRandom(-1.f, 1.f), ofRandom(-1.f, 1.f), ofRandom(-1.f, 1.f)).normalized());
		btTransform trans = ofGetBtTransform(pos, quat);
		shapes[i]->getRigidBody()->setWorldTransform(trans);
		shapes[i]->getRigidBody()->getMotionState()->setWorldTransform(trans);
	}
}

実行結果はこんな感じ。
何かキーを押すと、位置姿勢がランダムに更新されます。衝突していれば黄色、していなければ白色でプリミティブが表示されます。
f:id:cvl-robot:20180521174122p:plain
f:id:cvl-robot:20180521174128p:plain

コメントアウトしてあるCapsuleやConeは、ofxBulletUtils.hにわずかな変更を加えると使えるようになります。

static btCapsuleShape* ofBtGetCapsuleCollisionShape(float a_radius, float a_height) {
	return new btCapsuleShape(a_radius, a_height);
}

static btConeShape* ofBtGetConeCollisionShape(float a_radius, float a_height) {
	return new btConeShape(a_radius, a_height);
}

また、ofxBulletのCylinderやCapsuleのcreate関数の姿勢の指定にはバグがあります。Quaternionと指定しているのに内部でAxis-Angleとして渡しています。トラブル防止のために、Create関数では姿勢を与えないほうが無難です。

[1] Awesome Collision Detection
github.com

[2] ofxBullet
github.com

[3] Bulletについて
Bullet - Game Science Wiki

[4] Bulletで強制的に位置姿勢を更新する方法
stackoverflow.com

[5] 衝突テストを行う物同士のラベル付けの方法
Bullet Physics Japanese Manual - 06

[6] Collision Filtering
Collision Filtering - Physics Simulation Wiki

[]
2018年5月15日 – プログラミング de 落書き

今日のマンガ

星崎真紀先生の、魔法のリノベ全3巻がリアルで面白い。発行部数が少ないのか書店で見かけることはまずありませんので、ネットで注文。

魔法のリノベ(2) (ジュールコミックス)

魔法のリノベ(2) (ジュールコミックス)

魔法のリノベ(3) (ジュールコミックス)

魔法のリノベ(3) (ジュールコミックス)