0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DirectX12でミクさんを躍らせてみよう12-物理エンジン

Last updated at Posted at 2024-10-01

前回

物理エンジン

こんにちは。前回の記事までで、ミクさんが踊るようにアニメーションを適用しました。

しかし、まだ髪の毛や服が動かないので、少し物足りない感じがします。

今回の記事では、髪の毛や服が物理シミュレーションされるようにしてみます。

ただし、物理シミュレーションを最初から直接実装するのはかなり難しいです。

そこで、既に作られている物理エンジンを私たちのプロジェクトに追加して使ってみることにしましょう。

Bullet Physics

Bullet Physicsは衝突検出、剛体および流体の物理的挙動をシミュレーションするオープンソースエンジンです。

ゲームだけでなく、シミュレーション、ロボット工学などの分野でも使用されているそうです。

では早速Bullet Physicsライブラリをダウンロードし、私たちのプロジェクトで使用できるようにビルドしましょう。

https://github.com/bulletphysics/bullet3/releases
上記のリンクからソースコードの圧縮ファイルをダウンロードしてください。

私が使用したバージョンは3.2.5です。

ダウンロードが完了したら、解凍してください。

今度はCMakeを使用してビルドする必要があります。

CMakeがインストールされていない場合は、CMakeもインストールしてください。

今回はデバッグビルドのみを使用する予定です。

コマンドプロンプトを開いて、解凍したパスに移動します。

そして、以下のようなコマンドを実行してください。

cmake -G "Visual Studio 16 2019" 
-A x64 
-D CMAKE_INSTALL_PREFIX="C:XXX\bullet3-3.25"
-D INSTALL_LIBS=ON 
-D USE_MSVC_RUNTIME_LIBRARY_DLL=ON 
-D BUILD_CPU_DEMOS=OFF 
-D BUILD_OPENGL3_DEMOS=OFF 
-D BUILD_BULLET2_DEMOS=OFF 
-D BUILD_UNIT_TESTS=OFF

-D CMAKE_INSTALL_PREFIXの"C:XXX\bullet3-3.25"の部分は、CMakeでビルドされたファイルがインストールされるディレクトリです。

私は解凍したフォルダを指定しました。

ここまで問題なければ、すぐにビルドも実行してください。

cmake --build . --config Debug --target ALL_BUILD
cmake --build . --config Debug --target INSTALL

ビルドに問題がなければ、設定したディレクトリにlibというフォルダが作成されているはずです。

次に、このライブラリファイルを私たちのプロジェクトで使用できるように追加しましょう。

image.png

プロパティ→リンカー→全般→追加のライブラリディレクトリに、ビルドされたライブラリファイルがあるフォルダのパスを入力します。

image 1.png
私の場合はこのパスでした。

システム環境変数に追加しました。

パスを直接入力しても、環境変数として追加しても問題ありません。

ヘッダーファイルも使用できるように追加します。

image 2.png
プロパティ→C/C++→全般→追加のインクルードディレクトリ にヘッダーファイルがあるディレクトリを追加します。

これから物理シミュレーションを使用するためのコードを書いていきます。

作成する必要がある新しいクラスは以下の通りです。

  • MotionState:ボーンの位置と回転を同期する
  • RigidBody:剛体クラス
  • Joint:関節クラス
  • PhysicsManager:BulletPhysicsを全体的に管理する

順番に作成していきます。

MotionStateクラス

BulletPhysicsで物理シミュレーションを行うための方法を非常に簡単に説明すると

  1. 物理シミュレーション用の空間を生成
  2. 剛体を生成
  3. 剛体にMotionStateを登録
  4. BulletPhysicsが物理演算を行いながら剛体の状態を更新し、それに伴いMotionStateも更新される。

このような感じです。

実際には剛体が本体ですが、剛体がシミュレーションされても、これが実際にBoneNodeに適用されてボーンが回転したり動いたりする必要があります。

MotionStateはそのためのクラスだと考えてください。

class MotionState : public btMotionState
{
public:
	virtual void Reset() = 0;
	virtual void ReflectGlobalTransform() = 0;
};

このMotionState抽象クラスを継承して、剛体のタイプに応じてクラスを作成します。

class DefaultMotionState : public MotionState
{
public:
	DefaultMotionState(const XMMATRIX& transform)
	{
		float m[16];
		MathUtil::GetTransposeMatrix(transform, m);
		_transform.setFromOpenGLMatrix(m);
		_initTransform = _transform;
	}

	void getWorldTransform(btTransform& worldTrans) const override
	{
		worldTrans = _transform;
	}

	void setWorldTransform(const btTransform& worldTrans) override
	{
		_transform = worldTrans;
	}

	void Reset() override
	{
		
	}

	void ReflectGlobalTransform() override
	{
		
	}

private:
	btTransform _initTransform;
	btTransform _transform;
};

これは剛体が影響を与えるボーンがない場合に使用されるMotionStateです。

Reset、ReflectGlobalTransformは私たちが直接呼び出すメソッドです。

getWorldTransformやsetWorldTransformはBullet Physicsが呼び出すメソッドです。

コンストラクタではXMMATRIXで変換行列を初期化していますが、GetTransposeMatrixというメソッドでfloatの配列に変換してbtTransformとして保存しています。

これはBullet Physicsが列優先行列を使用しているためです。

DirectXは行優先行列を使用しているため、これを転置して変換するためです。

XMMATRIXを転置してfloatの配列として返すメソッドは次のとおりです。

void GetTransposeMatrix(const XMMATRIX& matrix, float* out)
	{
		XMMATRIX m = XMMatrixTranspose(matrix);

		out[0] = m.r[0].m128_f32[0];
		out[1] = m.r[1].m128_f32[0];
		out[2] = m.r[2].m128_f32[0];
		out[3] = m.r[3].m128_f32[0];
		out[4] = m.r[0].m128_f32[1];
		out[5] = m.r[1].m128_f32[1];
		out[6] = m.r[2].m128_f32[1];
		out[7] = m.r[3].m128_f32[1];
		out[8] = m.r[0].m128_f32[2];
		out[9] = m.r[1].m128_f32[2];
		out[10] = m.r[2].m128_f32[2];
		out[11] = m.r[3].m128_f32[2];
		out[12] = m.r[0].m128_f32[3];
		out[13] = m.r[1].m128_f32[3];
		out[14] = m.r[2].m128_f32[3];
		out[15] = m.r[3].m128_f32[3];
	}
class DynamicMotionState : public MotionState
{
public:
	DynamicMotionState(BoneNode* boneNode, const XMMATRIX& offset, bool override = true) :
	_boneNode(boneNode),
	_offset(offset),
	_override(override)
	{
		if (_boneNode->GetBoneIndex() == 136)
		{
			int d = 0;
		}

		XMVECTOR determinant = XMMatrixDeterminant(_offset);
		_invOffset = XMMatrixInverse(&determinant, _offset);

		Reset();
	}

	void getWorldTransform(btTransform& worldTransform) const override
	{
		worldTransform = _transform;
	}

	void setWorldTransform(const btTransform& worldTrans) override
	{
		_transform = worldTrans;
	}

	void Reset() override
	{
		XMMATRIX global = _offset * _boneNode->GetGlobalTransform();
		float m[16];
		MathUtil::GetTransposeMatrix(global, m);
		_transform.setFromOpenGLMatrix(m);
	}

	void ReflectGlobalTransform() override
	{
		XMMATRIX world = MathUtil::GetMatrixFrombtTransform(_transform);
		XMMATRIX result = _invOffset * world;

		if (_override == true)
		{
			_boneNode->SetGlobalTransform(result);
			_boneNode->UpdateChildTransform();
		}
	}

private:
	BoneNode* _boneNode;
	XMMATRIX _offset;
	XMMATRIX _invOffset;
	btTransform _transform;
	bool _override;
};

DynamicMotionStateはコンストラクタでBoneNodeを受け取って初期化し、

ReflectGlobalTransformでBoneNodeに物理シミュレーションの結果を反映しています。

GetMatrixFrombtTransformは先ほどとは逆に、btTransformをXMMATRIXに変換するメソッドです。

DirectX::XMMATRIX& GetMatrixFrombtTransform(btTransform& btTransform)
	{
		float m[16];
		btTransform.getOpenGLMatrix(m);

		XMMATRIX resultMatrix;
		resultMatrix.r[0].m128_f32[0] = m[0];
		resultMatrix.r[1].m128_f32[0] = m[1];
		resultMatrix.r[2].m128_f32[0] = m[2];
		resultMatrix.r[3].m128_f32[0] = m[3];

		resultMatrix.r[0].m128_f32[1] = m[4];
		resultMatrix.r[1].m128_f32[1] = m[5];
		resultMatrix.r[2].m128_f32[1] = m[6];
		resultMatrix.r[3].m128_f32[1] = m[7];

		resultMatrix.r[0].m128_f32[2] = m[8];
		resultMatrix.r[1].m128_f32[2] = m[9];
		resultMatrix.r[2].m128_f32[2] = m[10];
		resultMatrix.r[3].m128_f32[2] = m[11];

		resultMatrix.r[0].m128_f32[3] = m[12];
		resultMatrix.r[1].m128_f32[3] = m[13];
		resultMatrix.r[2].m128_f32[3] = m[14];
		resultMatrix.r[3].m128_f32[3] = m[15];

		resultMatrix = XMMatrixTranspose(resultMatrix);

		return resultMatrix;
	}

class DynamicAndBoneMergeMotionState : public MotionState
{
public:
	DynamicAndBoneMergeMotionState(BoneNode* boneNode, const XMMATRIX& offset, bool override = true):
	_boneNode(boneNode),
	_offset(offset),
	_override(override)
	{
		XMVECTOR determinant = XMMatrixDeterminant(_offset);
		_invOffset = XMMatrixInverse(&determinant, offset);
		Reset();
	}

	void getWorldTransform(btTransform& worldTrans) const override
	{
		worldTrans = _transform;
	}

	void setWorldTransform(const btTransform& worldTrans) override
	{
		_transform = worldTrans;
	}

	void Reset() override
	{
		XMMATRIX global = _offset * _boneNode->GetGlobalTransform();
		float m[16];
		MathUtil::GetTransposeMatrix(global, m);
		_transform.setFromOpenGLMatrix(m);
	}

	void ReflectGlobalTransform() override
	{
		XMMATRIX world = MathUtil::GetMatrixFrombtTransform(_transform);
		XMMATRIX result = _invOffset * world;
		XMMATRIX global = _boneNode->GetGlobalTransform();

		result.r[3] = global.r[3];

		if (_override == true)
		{
			_boneNode->SetGlobalTransform(result);
			_boneNode->UpdateChildTransform();
		}
	}

private:
	BoneNode* _boneNode;
	XMMATRIX _offset;
	XMMATRIX _invOffset;
	btTransform _transform;
	bool _override;
};

DynamicAndBoneMergeMotionStateは物理状態とボーンの状態を合わしてBoneNodeに反映しています。

class KinematicMotionState : public MotionState
{
public:
	KinematicMotionState(BoneNode* node, const XMMATRIX& offset):
	_boneNode(node),
	_offset(offset)
	{
	}

	void getWorldTransform(btTransform& worldTrans) const override
	{
		XMMATRIX matrix;
		if (_boneNode != nullptr)
		{
			matrix = _offset * _boneNode->GetGlobalTransform();
		}
		else
		{
			matrix = _offset;
		}

		float m[16];
		MathUtil::GetTransposeMatrix(matrix, m);
		worldTrans.setFromOpenGLMatrix(m);
	}

	void setWorldTransform(const btTransform& worldTrans) override
	{
	}

	void Reset() override
	{
	}

	void ReflectGlobalTransform() override
	{
	}

private:
	BoneNode* _boneNode;
	XMMATRIX _offset;
};

KinematicMotionStateは、物理的に直接計算されず、使用者が指定した変換値に従って動く特性を持っています。

必要なMotionStateをすべて作成しましたが、BoneNodeのグローバル変換がMotionStateによって更新される際に、それが子ボーンのBoneNodeにも適用されるように、BoneNodeにUpdateChildTransformも追加しましょう。

void BoneNode::UpdateChildTransform()
{
	for (BoneNode* child : _childrenNodes)
	{
		child->UpdateGlobalTransform();
	}
}

RigidBody クラス

剛体クラスです。

PMXデータにPMXRigidBodyというものがありました。

これを利用してBullet Physicsで使用される剛体を初期化し管理するクラスです。

class RigidBody
{
public:
	RigidBody();
	~RigidBody();

	bool Init(const PMXRigidBody& pmxRigidBody, NodeManager* nodeManager, BoneNode* boneNode);

	btRigidBody* GetRigidBody() const;
	unsigned short GetGroup() const;
	unsigned short GetGroupMask() const;

	void SetActive(bool active);
	void Reset(btDiscreteDynamicsWorld* world);
	void ResetTransform();
	void ReflectGlobalTransform();
	void CalcLocalTransform();

	DirectX::XMMATRIX& GetTransform();

private:
	enum class RigidBodyType
	{
		Kinematic,
		Dynamic,
		Aligned,
	};

private:
	std::unique_ptr<btCollisionShape> _shape;
	std::unique_ptr<MotionState> _activeMotionState;
	std::unique_ptr<MotionState> _kinematicMotionState;
	std::unique_ptr<btRigidBody> _rigidBody;

	RigidBodyType _rigidBodyType;
	unsigned short _group;
	unsigned short _groupMask;

	BoneNode* _node;
	DirectX::XMFLOAT4X4 _offsetMatrix;

	std::wstring _name;
};

初期化であるInitメソッドから作成しましょう。

bool RigidBody::Init(const PMXRigidBody& pmxRigidBody, NodeManager* nodeManager, BoneNode* boneNode)
{
	_shape = nullptr;

	switch (pmxRigidBody.shape)
	{
	case PMXRigidBody::Shape::Sphere:
		_shape = std::make_unique<btSphereShape>(pmxRigidBody.shapeSize.x);
		break;
	case PMXRigidBody::Shape::Box:
		_shape = std::make_unique<btBoxShape>(btVector3(pmxRigidBody.shapeSize.x, pmxRigidBody.shapeSize.y, pmxRigidBody.shapeSize.z));
		break;
	case PMXRigidBody::Shape::Capsule:
		_shape = std::make_unique<btCapsuleShape>(pmxRigidBody.shapeSize.x, pmxRigidBody.shapeSize.y);
		break;
	default:
		break;
	}

	if (_shape == nullptr)
	{
		return false;
	}

	btScalar mass(0.f);
	btVector3 localInertia(0.f, 0.f, 0.f);

	if (pmxRigidBody.op != PMXRigidBody::Operation::Static)
	{
		mass = pmxRigidBody.mass;
	}

	if (mass != 0.f)
	{
		_shape->calculateLocalInertia(mass, localInertia);
	}

	DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationRollPitchYaw(pmxRigidBody.rotate.x, pmxRigidBody.rotate.y, pmxRigidBody.rotate.z);
	DirectX::XMMATRIX translate = DirectX::XMMatrixTranslation(pmxRigidBody.translate.x, pmxRigidBody.translate.y, pmxRigidBody.translate.z);

	DirectX::XMMATRIX rigidBodyMat = rotation * translate;

	BoneNode* kinematicNode = nullptr;
	if (boneNode != nullptr)
	{
		XMVECTOR determinant = XMMatrixDeterminant(boneNode->GetGlobalTransform());
		XMStoreFloat4x4(&_offsetMatrix, rigidBodyMat * XMMatrixInverse(&determinant, boneNode->GetGlobalTransform()));
		kinematicNode = boneNode;
	}
	else
	{
		BoneNode* root = nodeManager->GetBoneNodeByIndex(0);
		XMVECTOR determinant = XMMatrixDeterminant(boneNode->GetGlobalTransform());
		XMStoreFloat4x4(&_offsetMatrix, rigidBodyMat * XMMatrixInverse(&determinant, boneNode->GetGlobalTransform()));
		kinematicNode = root;
	}

	XMMATRIX xmOffsetMatrix = XMLoadFloat4x4(&_offsetMatrix);
	btMotionState* motionState = nullptr;
	if (pmxRigidBody.op == PMXRigidBody::Operation::Static)
	{
		_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
		motionState = _kinematicMotionState.get();
	}
	else
	{
		if (boneNode != nullptr)
		{
			if (pmxRigidBody.op == PMXRigidBody::Operation::Dynamic)
			{
				_activeMotionState = std::make_unique<DynamicMotionState>(kinematicNode, xmOffsetMatrix);
				_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
				motionState = _activeMotionState.get();
			}
			else if (pmxRigidBody.op == PMXRigidBody::Operation::DynamicAndBoneMerge)
			{
				_activeMotionState = std::make_unique<DynamicAndBoneMergeMotionState>(kinematicNode, xmOffsetMatrix);
				_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
				motionState = _activeMotionState.get();
			}
		}
		else
		{
			_activeMotionState = std::make_unique<DefaultMotionState>(xmOffsetMatrix);
			_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
			motionState = _activeMotionState.get();
		}
	}

	btRigidBody::btRigidBodyConstructionInfo rigidBodyInfo(mass, motionState, _shape.get(), localInertia);
	rigidBodyInfo.m_linearDamping = pmxRigidBody.translateDimmer;
	rigidBodyInfo.m_angularDamping = pmxRigidBody.rotateDimmer;
	rigidBodyInfo.m_restitution = pmxRigidBody.repulsion;
	rigidBodyInfo.m_friction = pmxRigidBody.friction;
	rigidBodyInfo.m_additionalDamping = true;

	_rigidBody = std::make_unique<btRigidBody>(rigidBodyInfo);
	_rigidBody->setUserPointer(this);
	_rigidBody->setSleepingThresholds(0.01f, XMConvertToRadians(0.1f));
	_rigidBody->setActivationState(DISABLE_DEACTIVATION);
	if (pmxRigidBody.op == PMXRigidBody::Operation::Static)
	{
		_rigidBody->setCollisionFlags(_rigidBody->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT);
	}

	_rigidBodyType = (RigidBodyType)pmxRigidBody.op;
	_group = pmxRigidBody.group;
	_groupMask = pmxRigidBody.collisionGroup;
	_node = boneNode;
	_name = pmxRigidBody.name;

	return true;
}
bool RigidBody::Init(const PMXRigidBody& pmxRigidBody, NodeManager* nodeManager, BoneNode* boneNode)

PMXRigidBodyから情報を受け取り、この情報で初期化されます。

剛体をこのように作成する理由は、結局のところ物理シミュレーションによってボーンを動かすためであり、そのためこの剛体の影響を受けるBoneNodeも受け取ります。

switch (pmxRigidBody.shape)
	{
	case PMXRigidBody::Shape::Sphere:
		_shape = std::make_unique<btSphereShape>(pmxRigidBody.shapeSize.x);
		break;
	case PMXRigidBody::Shape::Box:
		_shape = std::make_unique<btBoxShape>(btVector3(pmxRigidBody.shapeSize.x, pmxRigidBody.shapeSize.y, pmxRigidBody.shapeSize.z));
		break;
	case PMXRigidBody::Shape::Capsule:
		_shape = std::make_unique<btCapsuleShape>(pmxRigidBody.shapeSize.x, pmxRigidBody.shapeSize.y);
		break;
	default:
		break;
	}

PMXRigidBodyのShape列挙体で剛体の形状が定義されています。

形状に合わせて形状オブジェクト(球、ボックス、カプセル)を生成します。

サイズはshapeSizeから取得できます。

	btScalar mass(0.f);
	btVector3 localInertia(0.f, 0.f, 0.f);

	if (pmxRigidBody.op != PMXRigidBody::Operation::Static)
	{
		mass = pmxRigidBody.mass;
	}

	if (mass != 0.f)
	{
		_shape->calculateLocalInertia(mass, localInertia);
	}

質量と慣性を初期化します。

静的(Static)な剛体でない場合のみ、massを取得して質量を初期化します。

	DirectX::XMMATRIX rotation = DirectX::XMMatrixRotationRollPitchYaw(pmxRigidBody.rotate.x, pmxRigidBody.rotate.y, pmxRigidBody.rotate.z);
	DirectX::XMMATRIX translate = DirectX::XMMatrixTranslation(pmxRigidBody.translate.x, pmxRigidBody.translate.y, pmxRigidBody.translate.z);

	DirectX::XMMATRIX rigidBodyMat = rotation * translate;

	BoneNode* kinematicNode = nullptr;
	if (boneNode != nullptr)
	{
		XMVECTOR determinant = XMMatrixDeterminant(boneNode->GetGlobalTransform());
		XMStoreFloat4x4(&_offsetMatrix, rigidBodyMat * XMMatrixInverse(&determinant, boneNode->GetGlobalTransform()));
		kinematicNode = boneNode;
	}
	else
	{
		BoneNode* root = nodeManager->GetBoneNodeByIndex(0);
		XMVECTOR determinant = XMMatrixDeterminant(boneNode->GetGlobalTransform());
		XMStoreFloat4x4(&_offsetMatrix, rigidBodyMat * XMMatrixInverse(&determinant, boneNode->GetGlobalTransform()));
		kinematicNode = root;
	}

剛体の回転と移動を取得して変換行列を作成します。

BoneNodeが正しく渡された場合、グローバル変換を取得してオフセット行列を計算し、

BoneNodeが渡されなかった場合、ルートノードを使用してオフセット行列を計算します。

	XMMATRIX xmOffsetMatrix = XMLoadFloat4x4(&_offsetMatrix);
	btMotionState* motionState = nullptr;
	if (pmxRigidBody.op == PMXRigidBody::Operation::Static)
	{
		_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
		motionState = _kinematicMotionState.get();
	}
	else
	{
		if (boneNode != nullptr)
		{
			if (pmxRigidBody.op == PMXRigidBody::Operation::Dynamic)
			{
				_activeMotionState = std::make_unique<DynamicMotionState>(kinematicNode, xmOffsetMatrix);
				_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
				motionState = _activeMotionState.get();
			}
			else if (pmxRigidBody.op == PMXRigidBody::Operation::DynamicAndBoneMerge)
			{
				_activeMotionState = std::make_unique<DynamicAndBoneMergeMotionState>(kinematicNode, xmOffsetMatrix);
				_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
				motionState = _activeMotionState.get();
			}
		}
		else
		{
			_activeMotionState = std::make_unique<DefaultMotionState>(xmOffsetMatrix);
			_kinematicMotionState = std::make_unique<KinematicMotionState>(kinematicNode, xmOffsetMatrix);
			motionState = _activeMotionState.get();
		}
	}

剛体のタイプに応じて適切なMotionStateを生成して初期化します。

	btRigidBody::btRigidBodyConstructionInfo rigidBodyInfo(mass, motionState, _shape.get(), localInertia);
	rigidBodyInfo.m_linearDamping = pmxRigidBody.translateDimmer;
	rigidBodyInfo.m_angularDamping = pmxRigidBody.rotateDimmer;
	rigidBodyInfo.m_restitution = pmxRigidBody.repulsion;
	rigidBodyInfo.m_friction = pmxRigidBody.friction;
	rigidBodyInfo.m_additionalDamping = true;

	_rigidBody = std::make_unique<btRigidBody>(rigidBodyInfo);
	_rigidBody->setUserPointer(this);
	_rigidBody->setSleepingThresholds(0.01f, XMConvertToRadians(0.1f));
	_rigidBody->setActivationState(DISABLE_DEACTIVATION);
	if (pmxRigidBody.op == PMXRigidBody::Operation::Static)
	{
		_rigidBody->setCollisionFlags(_rigidBody->getCollisionFlags() | btCollisionObject::CF_KINEMATIC_OBJECT);
	}

	_rigidBodyType = (RigidBodyType)pmxRigidBody.op;
	_group = pmxRigidBody.group;
	_groupMask = pmxRigidBody.collisionGroup;
	_node = boneNode;
	_name = pmxRigidBody.name;

最後に剛体の設定を行い、名前やグループなどの設定を行います。

剛体を活性化するメソッドを作成します。

void RigidBody::SetActive(bool active)
{
	if (_rigidBodyType != RigidBodyType::Kinematic)
	{
		if (active == true)
		{
			_rigidBody->setMotionState(_activeMotionState.get());
		}
		else
		{
			_rigidBody->setMotionState(_kinematicMotionState.get());
		}
	}
	else
	{
		_rigidBody->setMotionState(_kinematicMotionState.get());
	}
}

無効化する際は、btRigidBodyである_rigidBodyにKinematicMotionStateを設定し、ボーンに剛体の状態が反映されないようにします。

void RigidBody::Reset(btDiscreteDynamicsWorld* world)
{
	btOverlappingPairCache* cache = world->getPairCache();
	if (cache != nullptr)
	{
		btDispatcher* dispatcher = world->getDispatcher();
		cache->cleanProxyFromPairs(_rigidBody->getBroadphaseHandle(), dispatcher);
	}

	_rigidBody->setAngularVelocity(btVector3(0, 0, 0));
	_rigidBody->setLinearVelocity(btVector3(0, 0, 0));
	_rigidBody->clearForces();
}

void RigidBody::ResetTransform()
{
	if (_activeMotionState != nullptr)
	{
		_activeMotionState->Reset();
	}
}

剛体をリセットするメソッドです。

void RigidBody::ReflectGlobalTransform()
{
	if (_activeMotionState != nullptr)
	{
		_activeMotionState->ReflectGlobalTransform();
	}

	if (_kinematicMotionState != nullptr)
	{
		_kinematicMotionState->ReflectGlobalTransform();
	}
}

void RigidBody::CalcLocalTransform()
{
	if (_node == nullptr)
	{
		return;
	}

	const BoneNode* parent = _node->GetParentBoneNode();
	if (parent == nullptr)
	{
		_node->SetLocalTransform(_node->GetGlobalTransform());
	}
	else
	{
		XMVECTOR determinant = XMMatrixDeterminant(parent->GetGlobalTransform());
		XMMATRIX local = _node->GetGlobalTransform() * XMMatrixInverse(&determinant, parent->GetGlobalTransform());
		_node->SetLocalTransform(local);
	}
}

これらは、ボーンに物理状態を反映させるためのメソッドです。

実際に呼び出される際は、まずReflectGlobalTransformを呼び出してボーンのグローバル変換を計算し、その後CalcLocalTransformを呼び出してボーンのローカル変換を計算します。

残りはすべてメンバーに対するゲッターなので、別途説明はしません。

Joint クラス

これは2つの剛体を接続するクラスです。

Unityエンジンにもジョイントがありますが、それで遊んだことがある方なら、どういうものかイメージがつくでしょう。

Jointクラスは、2つの剛体を受け取り、Bullet Physicsに追加するbtTypedConstraintを生成することがほとんどすべてです。

class Joint
{
public:
	Joint();
	~Joint();
	Joint(const Joint& other) = delete;
	Joint& operator = (const Joint& other) = delete;

	bool CreateJoint(const PMXJoint& pmxJoint, RigidBody* rigidBody0, RigidBody* rigidBody1);
	void Destroy();

	btTypedConstraint* GetConstraint() const;

private:
	std::unique_ptr<btTypedConstraint> _constraint;
};

宣言部です。

bool Joint::CreateJoint(const PMXJoint& pmxJoint, RigidBody* rigidBody0, RigidBody* rigidBody1)
{
	Destroy();

	btMatrix3x3 rotation;
	rotation.setEulerZYX(pmxJoint.rotate.x, pmxJoint.rotate.y, pmxJoint.rotate.z);

	btTransform transform;
	transform.setIdentity();
	transform.setOrigin(btVector3(pmxJoint.translate.x, pmxJoint.translate.y, pmxJoint.translate.z));
	transform.setBasis(rotation);

	btTransform inv0 = rigidBody0->GetRigidBody()->getWorldTransform().inverse();
	btTransform inv1 = rigidBody1->GetRigidBody()->getWorldTransform().inverse();
	inv0 = inv0 * transform;
	inv1 = inv1 * transform;

	auto constraint = std::make_unique<btGeneric6DofSpringConstraint>(*rigidBody0->GetRigidBody(), *rigidBody1->GetRigidBody(), inv0, inv1, true);
	constraint->setLinearLowerLimit(btVector3(pmxJoint.translateLowerLimit.x, pmxJoint.translateLowerLimit.y, pmxJoint.translateLowerLimit.z));
	constraint->setLinearUpperLimit(btVector3(pmxJoint.translateUpperLimit.x, pmxJoint.translateUpperLimit.y, pmxJoint.translateUpperLimit.z));
	constraint->setAngularLowerLimit(btVector3(pmxJoint.rotateLowerLimit.x, pmxJoint.rotateLowerLimit.y, pmxJoint.rotateLowerLimit.z));
	constraint->setAngularUpperLimit(btVector3(pmxJoint.rotateUpperLimit.x, pmxJoint.rotateUpperLimit.y, pmxJoint.rotateUpperLimit.z));

	if (pmxJoint.springTranslateFactor.x != 0)
	{
		constraint->enableSpring(0, true);
		constraint->setStiffness(0, pmxJoint.springTranslateFactor.x);
	}

	if (pmxJoint.springTranslateFactor.y != 0)
	{
		constraint->enableSpring(1, true);
		constraint->setStiffness(1, pmxJoint.springTranslateFactor.y);
	}

	if (pmxJoint.springTranslateFactor.z != 0)
	{
		constraint->enableSpring(2, true);
		constraint->setStiffness(2, pmxJoint.springTranslateFactor.z);
	}

	if (pmxJoint.springRotateFactor.x != 0)
	{
		constraint->enableSpring(3, true);
		constraint->setStiffness(3, pmxJoint.springRotateFactor.x);
	}

	if (pmxJoint.springRotateFactor.y != 0)
	{
		constraint->enableSpring(4, true);
		constraint->setStiffness(4, pmxJoint.springRotateFactor.y);
	}

	if (pmxJoint.springRotateFactor.z != 0)
	{
		constraint->enableSpring(5, true);
		constraint->setStiffness(5, pmxJoint.springRotateFactor.z);
	}

	_constraint = std::move(constraint);

	return true;
}

既存の_constraintがある場合は削除し、

2つの剛体間の関節を初期化するために回転と位置を設定します。

2つの剛体の相対的な変換を計算して相対的な位置と回転を取得し、

剛体間のスプリング関節を生成します。

生成したジョイントに対して移動および回転制限を設定します。

スプリングの弾性まで適用すると、ジョイントの生成が完了します。

このように生成されたジョイントは、後でBullet Physicsに登録され、自動的に物理演算が行われることになります。

PhysicsManager クラス

Bullet Physicsの物理世界の生成、剛体やジョイントの追加・削除、物理シミュレーションの更新を行うクラスです。

そして、物理シミュレーションを別のスレッドで実行するように管理します。

クラスの宣言部を見てみましょう。

class PhysicsManager
{
public:
	PhysicsManager();
	~PhysicsManager();

	PhysicsManager(const PhysicsManager& other) = delete;
	PhysicsManager& operator = (const PhysicsManager& other) = delete;

	static bool Create();
	static void Destroy();

	static void SetMaxSubStepCount(int numSteps);
	static int GetMaxSubStepCount();
	static void SetFixedTimeStep(float fixedTimeStep);
	static float GetFixedTimeStep();

	static void ActivePhysics(bool active);
	static void ForceUpdatePhysics();

	static void AddRigidBody(RigidBody* rigidBody);
	static void RemoveRigidBody(RigidBody* rigidBody);
	static void AddJoint(Joint* joint);
	static void RemoveJoint(Joint* joint);

	static btDiscreteDynamicsWorld* GetDynamicsWorld();

private:
	static void UpdateByThread();

private:
	static std::unique_ptr<btDiscreteDynamicsWorld> _world;
	static std::unique_ptr<btBroadphaseInterface> _broadPhase;
	static std::unique_ptr<btDefaultCollisionConfiguration> _collisionConfig;
	static std::unique_ptr<btCollisionDispatcher> _dispatcher;
	static std::unique_ptr<btSequentialImpulseConstraintSolver> _solver;
	static std::unique_ptr<btCollisionShape> _groundShape;
	static std::unique_ptr<btMotionState> _groundMS;
	static std::unique_ptr<btRigidBody> _groundRB;
	static std::unique_ptr<btOverlapFilterCallback> _filterCB;

	static float _fixedTimeStep;
	static int _maxSubStepCount;

	static std::thread _physicsUpdateThread;
	static bool _threadFlag;

	static std::atomic<bool> _stopFlag;
	static std::atomic<bool> _endFlag;
};

Bullet Physicsのオブジェクトを保持し、別スレッドでの更新を安全に終了させるためのフラグも持っています。

Bullet Physics オブジェクトを使用するためにヘッダーファイルをインクルードします。

#include <BulletDynamics/ConstraintSolver/btSequentialImpulseConstraintSolverMt.h>
#include "BulletDynamics/MLCPSolvers/btDantzigSolver.h"
#include "BulletDynamics/Dynamics/btDiscreteDynamicsWorldMt.h"
#include "BulletDynamics/ConstraintSolver/btContactSolverInfo.h"

ほとんどがstaticメンバーであるため、以下のようにcppファイルで初期化します。

std::unique_ptr<btDiscreteDynamicsWorld> PhysicsManager::_world = nullptr;
std::unique_ptr<btBroadphaseInterface> PhysicsManager::_broadPhase = nullptr;
std::unique_ptr<btDefaultCollisionConfiguration> PhysicsManager::_collisionConfig = nullptr;
std::unique_ptr<btCollisionDispatcher> PhysicsManager::_dispatcher = nullptr;
std::unique_ptr<btSequentialImpulseConstraintSolver> PhysicsManager::_solver = nullptr;
std::unique_ptr<btCollisionShape> PhysicsManager::_groundShape = nullptr;
std::unique_ptr<btMotionState> PhysicsManager::_groundMS = nullptr;
std::unique_ptr<btRigidBody> PhysicsManager::_groundRB = nullptr;
std::unique_ptr<btOverlapFilterCallback> PhysicsManager::_filterCB = nullptr;

float PhysicsManager::_fixedTimeStep = 0.02f;
int PhysicsManager::_maxSubStepCount = 5;

std::thread PhysicsManager::_physicsUpdateThread = std::thread();
bool PhysicsManager::_threadFlag = false;

std::atomic<bool> PhysicsManager::_stopFlag(false);
std::atomic<bool> PhysicsManager::_endFlag(false);
bool PhysicsManager::Create()
{
	_broadPhase = std::make_unique<btDbvtBroadphase>();
	_collisionConfig = std::make_unique<btDefaultCollisionConfiguration>();
	_dispatcher = std::make_unique<btCollisionDispatcher>(_collisionConfig.get());
	_solver = std::make_unique<btSequentialImpulseConstraintSolver>();

	_world = std::make_unique<btDiscreteDynamicsWorld>(_dispatcher.get(), _broadPhase.get(), _solver.get(), _collisionConfig.get());
	_world->setGravity(btVector3(0, -9.8f * 10.0f, 0));

	_groundShape = std::make_unique<btStaticPlaneShape>(btVector3(0.f, 1.f, 0.f), 0.f);

	btTransform groundTransform;
	groundTransform.setIdentity();

	_groundMS = std::make_unique<btDefaultMotionState>(groundTransform);

	btRigidBody::btRigidBodyConstructionInfo groundInfo(0.f, _groundMS.get(), _groundShape.get(), btVector3(0.f, 0.f, 0.f));
	_groundRB = std::make_unique<btRigidBody>(groundInfo);

	_world->addRigidBody(_groundRB.get());

	auto filterCB = std::make_unique<FilterCallback>();
	filterCB->NonFilterProxy.push_back(_groundRB->getBroadphaseProxy());
	_world->getPairCache()->setOverlapFilterCallback(filterCB.get());
	_filterCB = std::move(filterCB);

	btContactSolverInfo& info = _world->getSolverInfo();
	info.m_numIterations = 4;
	info.m_solverMode = SOLVER_SIMD;

	return true;
}

これはBullet Physicsの物理世界を生成し初期化するメソッドです。

衝突処理に必要な要素であるbtDbvtBroadphasebtDefaultCollisionConfigurationbtCollisionDispatcherbtSequentialImpulseConstraintSolverなどを設定します。

重力を設定し、地面を生成して物理世界に追加します。

そして、衝突フィルタリングコールバックを設定しています。

これは特定のオブジェクト同士が衝突しないように設定する構造体です。

struct FilterCallback : public btOverlapFilterCallback
{
	bool needBroadphaseCollision(btBroadphaseProxy* proxy0, btBroadphaseProxy* proxy1) const override
	{
		auto findIterator = std::find_if(NonFilterProxy.begin(), NonFilterProxy.end(),
			[proxy0, proxy1](const auto& x)
			{
				return x == proxy0 || x == proxy1;
			});

		if (findIterator != NonFilterProxy.end())
		{
			return true;
		}

		bool collides = (proxy0->m_collisionFilterGroup & proxy1->m_collisionFilterMask) != 0;
		collides = collides && (proxy1->m_collisionFilterGroup & proxy0->m_collisionFilterMask);
		return collides;
	}

	std::vector<btBroadphaseProxy*> NonFilterProxy;
};

needBroadphaseCollisionで2つの物体が衝突すべきかどうかを決定します。

NonFilterProxyに含まれる物体は常に他の物体と衝突できるため、別途チェックしません。

そのため、Createで地面をNonFilterProxyに追加しています。

最後の部分は物理衝突の計算繰り返し回数と物理エンジンの演算方式を設定します。

SOLVER_SIMDはCPUの並列処理機能を活用するモードです。

CPUがSIMD演算をサポートしていなければ意味がありませんが、最近のCPUはほとんどサポートしているはずです。

void PhysicsManager::SetMaxSubStepCount(int numSteps)
{
	_maxSubStepCount = numSteps;
}

int PhysicsManager::GetMaxSubStepCount()
{
	return _maxSubStepCount;
}

void PhysicsManager::SetFixedTimeStep(float fixedTimeStep)
{
	_fixedTimeStep = fixedTimeStep;
}

float PhysicsManager::GetFixedTimeStep()
{
	return _fixedTimeStep;
}

btDiscreteDynamicsWorld* PhysicsManager::GetDynamicsWorld()
{
	return _world.get();
}

void PhysicsManager::AddRigidBody(RigidBody* rigidBody)
{
	_world->addRigidBody(rigidBody->GetRigidBody(), 1 << rigidBody->GetGroup(), rigidBody->GetGroupMask());
}

void PhysicsManager::RemoveRigidBody(RigidBody* rigidBody)
{
	_world->removeRigidBody(rigidBody->GetRigidBody());
}

void PhysicsManager::AddJoint(Joint* joint)
{
	if (joint->GetConstraint() == nullptr)
	{
		return;
	}

	_world->addConstraint(joint->GetConstraint());
}

void PhysicsManager::RemoveJoint(Joint* joint)
{
	if (joint->GetConstraint() == nullptr)
	{
		return;
	}

	_world->removeConstraint(joint->GetConstraint());
}

これらのメソッドはメンバーのゲッターメソッドや、剛体とジョイントを追加または削除するメソッドです。

void PhysicsManager::ActivePhysics(bool active)
{
	if (active == true)
	{
		_stopFlag.store(false);
		_endFlag.store(false);
		_threadFlag = true;
		_physicsUpdateThread = std::thread(UpdateByThread);
		_physicsUpdateThread.detach();
	}
	else
	{
		_stopFlag.store(true);
		while (_endFlag.load() == false);
		_threadFlag = false;
	}
}

void PhysicsManager::UpdateByThread()
{
	unsigned long prevTime = timeGetTime();

	while (true)
	{
		if (_stopFlag.load() == true)
		{
			break;
		}

		unsigned long currentTime = timeGetTime();
		unsigned long deltaTime = currentTime - prevTime;
		prevTime = currentTime;

		_world->stepSimulation(deltaTime * 0.001f, _maxSubStepCount, _fixedTimeStep);
	}

	_endFlag.store(true);
}

物理シミュレーションをアクティブにするメソッドと物理シミュレーション更新メソッドです。

std::atomic変数を使用して、物理シミュレーションメソッドが安全に終了するように処理しています。

ActivePhysicsのパラメータがtrueの場合、フラグを初期化し、別のスレッドで独立して物理シミュレーションが更新されるようにします。

パラメータがfalseの場合、_stopFlagをtrueに設定し、UpdateByThreadで_endFlagがtrueに設定されるまで待機します。

UpdateByThreadでは現在時間から前回の時間を引いて経過時間を求めます。

そして、stepSimulationに経過時間、最大サブステップ数、固定時間ステップを渡して物理シミュレーションを呼び出します。

経過時間はミリ秒単位であるため、0.001を掛けて変換します。

最大サブステップは、更新間の時間が長くなった場合に正確でない結果が出る可能性があるため、経過時間が長くなると複数回の間隔でより正確な物理計算を行うためのパラメータです。

この値が大きくなるほど、性能に計算回数が多くなる可能性があります。

固定時間ステップは、サブステップを使用する際にどの時間の間物理計算を行うかを決定する値です。

値が小さければ小さいほど、より正確なシミュレーション結果が得られますが、その分計算負荷が大きくなります。

void PhysicsManager::Destroy()
{
	if (_threadFlag == true)
	{
		_stopFlag.store(true);

		while (_endFlag.load() == false);
	}

	if (_world != nullptr && _groundRB != nullptr)
	{
		_world->removeRigidBody(_groundRB.get());
	}

	_world = nullptr;
	_broadPhase = nullptr;
	_collisionConfig = nullptr;
	_dispatcher = nullptr;
	_solver = nullptr;
	_groundShape = nullptr;
	_groundMS = nullptr;
	_groundRB = nullptr;
}

Destroyが呼び出されたときに安全にオブジェクトが削除されるよう、物理シミュレーションスレッドが終了するのを待ちます。

ここまで書けば物理エンジンを使用するための準備は完了です。

これから実際に使用するために剛体とジョイントを生成して登録し、結果が適用されるようにPMXActorを修正します。

PMXActorの修正

std::vector<std::unique_ptr<RigidBody>> _rigidBodies;
std::vector<std::unique_ptr<Joint>> _joints;

剛体とジョイントをメンバーとして持つように追加します。

void InitPhysics(const PMXFileData& pmxFileData);
void ResetPhysics();
void UpdatePhysicsAnimation(DWORD elapse);

物理演算に関するメソッドを追加します。

bool PMXActor::Initialize(const std::wstring& filePath, Dx12Wrapper& dx)
{
   ...
   InitPhysics(_pmxFileData);
   ...
   ResetPhysics();
}

Initialize メソッドで InitPhysics を呼び出します。

最後に ResetPhysics を呼び出します。

void PMXActor::InitPhysics(const PMXFileData& pmxFileData)
{
	PhysicsManager::ActivePhysics(false);

	for (const PMXRigidBody& pmxRigidBody : pmxFileData.rigidBodies)
	{
		RigidBody* rigidBody = new RigidBody();
		_rigidBodies.emplace_back(std::move(rigidBody));

		BoneNode* boneNode = nullptr;
		if (pmxRigidBody.boneIndex != -1)
		{
			boneNode = _nodeManager.GetBoneNodeByIndex(pmxRigidBody.boneIndex);
		}

		if (rigidBody->Init(pmxRigidBody, &_nodeManager, boneNode) == false)
		{
			OutputDebugStringA("Create Rigid Body Fail");
			continue;
		}
		PhysicsManager::AddRigidBody(rigidBody);
	}

	for (const PMXJoint& pmxJoint : pmxFileData.joints)
	{
		if (pmxJoint.rigidBodyAIndex != -1 &&
			pmxJoint.rigidBodyBIndex != -1 &&
			pmxJoint.rigidBodyAIndex != pmxJoint.rigidBodyBIndex)
		{
			Joint* joint = new Joint();
			_joints.emplace_back(std::move(joint));

			RigidBody* rigidBodyA = nullptr;
			if (_rigidBodies.size() <= pmxJoint.rigidBodyAIndex)
			{
				OutputDebugStringA("Create Joint Fail");
				continue;
			}

			rigidBodyA = _rigidBodies[pmxJoint.rigidBodyAIndex].get();

			RigidBody* rigidBodyB = nullptr;
			if (_rigidBodies.size() <= pmxJoint.rigidBodyBIndex)
			{
				OutputDebugStringA("Create Joint Fail");
				continue;
			}

			rigidBodyB = _rigidBodies[pmxJoint.rigidBodyBIndex].get();

			if (rigidBodyA->GetRigidBody()->isStaticOrKinematicObject() == true &&
				rigidBodyB->GetRigidBody()->isStaticOrKinematicObject() == true)
			{
				int d = 0;
			}

			if (joint->CreateJoint(pmxJoint, rigidBodyA, rigidBodyB) == false)
			{
				OutputDebugStringA("Create Joint Fail");
				continue;
			}

			PhysicsManager::AddJoint(joint);
		}
	}

	PhysicsManager::ActivePhysics(true);
}

PMXFileDataにはPMXRigidBodyとPMXJointでそれぞれ剛体とジョイントに関するデータが含まれています。

これらを使用して、適切にRigidBodyとJointを生成し、PhysicsManagerに追加します。

剛体やジョイントを追加する際に物理シミュレーションが更新されていると、エラーが発生する可能性があるため、初期化前にPhysicsManager::ActivePhysics(false)を呼び出して無効化し、追加が完了したら有効化します。

void PMXActor::ResetPhysics()
{
	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->SetActive(false);
		rigidBody->ResetTransform();
	}

	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->ReflectGlobalTransform();
	}

	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->CalcLocalTransform();
	}

	const auto& nodes = _nodeManager.GetAllNodes();
	for (const auto& node : nodes)
	{
		if (node->GetParentBoneNode() == nullptr)
		{
			node->UpdateGlobalTransform();
		}
	}

	btDiscreteDynamicsWorld* world = PhysicsManager::GetDynamicsWorld();
	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->Reset(world);
	}
}

すべての剛体を初期化します。

void PMXActor::UpdatePhysicsAnimation(DWORD elapse)
{
	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->SetActive(true);
	}

	//_physicsManager.Update(elapse);

	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->ReflectGlobalTransform();
	}

	for (auto& rigidBody : _rigidBodies)
	{
		rigidBody->CalcLocalTransform();
	}

	const auto& nodes = _nodeManager.GetAllNodes();
	for (const auto& node : nodes)
	{
		if (node->GetParentBoneNode() == nullptr)
		{
			node->UpdateGlobalTransform();
		}
	}
}

UpdatePhysicsAnimationは剛体を活性化し、ボーンのグローバル変換を更新します。

これからUpdatePhysicsAnimationを呼び出して物理状態を適用する必要がありますが、物理演算が追加されたことでボーンの更新順序が少し変わります。

PMXBoneを使ってBoneNodeを初期化する際に、ボーンフラグに応じて設定を変えたことを覚えていますか。

このフラグの中にPMXBoneFlags::DeformAfterPhysicsがあります。

このフラグは、該当するボーンが物理演算の前に更新されるべきかどうかを決定します。

そのため、NodeManagerでは物理演算の前に更新する必要があるボーンの更新と、物理演算の後に更新する必要があるボーンの更新とでメソッドを分離する必要があります。

まず、BoneNodeにこのフラグを設定できるようにゲッター、セッターメソッドを追加します。

class BoneNode
{
public:
  ...
  	void SetDeformAfterPhysics(bool deformAfterPhysics) { _deformAfterPhysics = deformAfterPhysics; }
	  bool GetDeformAfterPhysics() const { return _deformAfterPhysics; }
  ...
}

NodeManagerのBoneNodeを初期化する部分でフラグを設定するようにします。

void NodeManager::Init(const std::vector<PMXBone>& bones)
{
	_boneNodeByIdx.resize(bones.size());
	_sortedNodes.resize(bones.size());

	for (int index = 0; index < bones.size(); index++)
	{
		const PMXBone& currentBoneData = bones[index];
		_boneNodeByIdx[index] = new BoneNode(index, currentBoneData);
		_boneNodeByName[_boneNodeByIdx[index]->GetName()] = _boneNodeByIdx[index];
		_sortedNodes[index] = _boneNodeByIdx[index];
	}

	for (int index = 0; index < _boneNodeByIdx.size(); index++)
	{
		//Parent Bone Set
		BoneNode* currentBoneNode = _boneNodeByIdx[index];

		unsigned int parentBoneIndex = currentBoneNode->GetParentBoneIndex();
		if (parentBoneIndex != 65535 && _boneNodeByIdx.size() > parentBoneIndex)
		{
			currentBoneNode->SetParentBoneNode(_boneNodeByIdx[currentBoneNode->GetParentBoneIndex()]);
		}

		const PMXBone& currentPmxBone = bones[index];

		bool deformAfterPhysics = (uint16_t)currentPmxBone.boneFlag & (uint16_t)PMXBoneFlags::DeformAfterPhysics;
		currentBoneNode->SetDeformAfterPhysics(deformAfterPhysics);
		
		...

NodeManagerでは、UpdateAnimationで物理演算の前に更新する必要のあるボーンのみを更新するように修正し、UpdateAnimationAfterPhysicsメソッドを追加します。

void NodeManager::UpdateAnimation()
{
	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == true)
		{
			continue;
		}

		curNode->UpdateLocalTransform();
	}

	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == true)
		{
			continue;
		}

		if (curNode->GetParentBoneNode() != nullptr)
		{
			continue;
		}

		curNode->UpdateGlobalTransform();
	}

	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == true)
		{
			continue;
		}

		if (curNode->GetAppendBoneNode() != nullptr)
		{
			curNode->UpdateAppendTransform();
			curNode->UpdateGlobalTransform();
		}

		IKSolver* curSolver = curNode->GetIKSolver();
		if (curSolver != nullptr)
		{
			curSolver->Solve();
			curNode->UpdateGlobalTransform();
		}
	}
}

BoneNodeのGetDeformAfterPhysicsを呼び出し、falseのボーンのみを更新します。

void NodeManager::UpdateAnimationAfterPhysics()
{
	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == false)
		{
			continue;
		}

		curNode->UpdateLocalTransform();
	}

	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == false)
		{
			continue;
		}

		if (curNode->GetParentBoneNode() != nullptr)
		{
			continue;
		}

		curNode->UpdateGlobalTransform();
	}

	for (BoneNode* curNode : _sortedNodes)
	{
		if (curNode->GetDeformAfterPhysics() == false)
		{
			continue;
		}

		if (curNode->GetAppendBoneNode() != nullptr)
		{
			curNode->UpdateAppendTransform();
			curNode->UpdateGlobalTransform();
		}

		IKSolver* curSolver = curNode->GetIKSolver();
		if (curSolver != nullptr)
		{
			curSolver->Solve();
			curNode->UpdateGlobalTransform();
		}
	}
}

逆にGetDeformAfterPhysicsを呼び出して、trueのボーンのみを更新します。

PMXActorに戻り、これまでに追加した内容を正しい順序で呼び出します。

void PMXActor::UpdateAnimation()
{
	 ...
	_nodeManager.BeforeUpdateAnimation();

	_morphManager.Animate(frameNo);
	_nodeManager.EvaluateAnimation(frameNo);

	_nodeManager.UpdateAnimation();

	UpdatePhysicsAnimation(elapsedTime);

	_nodeManager.UpdateAnimationAfterPhysics();
  
  ...
}

ここまで完了したように見えますが、おそらくVertexSkinningByRangeメソッドでコンパイルエラーが発生するでしょう。

XMVECTORの四則演算を行う部分で、演算子にエラーが出るはずです。

これはBullet Physicsライブラリの演算子オーバーロードと衝突するためです。

XMVECTORも大半の四則演算子が演算子オーバーロードで実装されています。

例えば、A + B は XMVectorAdd(A, B)が演算子オーバーロードされたものです。

面倒ですが、エラーが発生する部分をすべて元のメソッド呼び出しに修正する必要があります。

void PMXActor::VertexSkinningByRange(const SkinningRange& range)
{
	for (unsigned int i = range.startIndex; i < range.startIndex + range.vertexCount; ++i)
	{
		const PMXVertex& currentVertexData = _pmxFileData.vertices[i];
		XMVECTOR position = XMLoadFloat3(&currentVertexData.position);
		XMVECTOR morphPosition = XMLoadFloat3(&_morphManager.GetMorphVertexPosition(i));

		switch (currentVertexData.weightType)
		{
		case PMXVertexWeight::BDEF1:
		{
			BoneNode* bone0 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[0]);
			XMMATRIX m0 = XMMatrixMultiply(bone0->GetInitInverseTransform(), bone0->GetGlobalTransform());
			position += morphPosition;
			position = XMVector3Transform(position, m0);
			break;
		}
		case PMXVertexWeight::BDEF2:
		{
			float weight0 = currentVertexData.boneWeights[0];
			float weight1 = 1.0f - weight0;

			BoneNode* bone0 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[0]);
			BoneNode* bone1 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[1]);

			XMMATRIX m0 = XMMatrixMultiply(bone0->GetInitInverseTransform(), bone0->GetGlobalTransform());
			XMMATRIX m1 = XMMatrixMultiply(bone1->GetInitInverseTransform(), bone1->GetGlobalTransform());

			XMMATRIX mat = m0 * weight0 + m1 * weight1;
			position += morphPosition;
			position = XMVector3Transform(position, mat);
			break;
		}
		case PMXVertexWeight::BDEF4:
		{
			float weight0 = currentVertexData.boneWeights[0];
			float weight1 = currentVertexData.boneWeights[1];
			float weight2 = currentVertexData.boneWeights[2];
			float weight3 = currentVertexData.boneWeights[3];

			BoneNode* bone0 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[0]);
			BoneNode* bone1 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[1]);
			BoneNode* bone2 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[2]);
			BoneNode* bone3 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[3]);

			XMMATRIX m0 = XMMatrixMultiply(bone0->GetInitInverseTransform(), bone0->GetGlobalTransform());
			XMMATRIX m1 = XMMatrixMultiply(bone1->GetInitInverseTransform(), bone1->GetGlobalTransform());
			XMMATRIX m2 = XMMatrixMultiply(bone2->GetInitInverseTransform(), bone2->GetGlobalTransform());
			XMMATRIX m3 = XMMatrixMultiply(bone3->GetInitInverseTransform(), bone3->GetGlobalTransform());

			XMMATRIX mat = m0 * weight0 + m1 * weight1 + m2 * weight2 + m3 * weight3;
			position += morphPosition;
			position = XMVector3Transform(position, mat);
			break;
		}
		case PMXVertexWeight::SDEF:
		{
			float w0 = currentVertexData.boneWeights[0];
			float w1 = 1.0f - w0;

			XMVECTOR sdefc = XMLoadFloat3(&currentVertexData.sdefC);
			XMVECTOR sdefr0 = XMLoadFloat3(&currentVertexData.sdefR0);
			XMVECTOR sdefr1 = XMLoadFloat3(&currentVertexData.sdefR1);

				//rw = sdefr0 * w0 + sdefr1 * w1
				//r0 = sdefc + sdefr0 - rw
				//r1 = sdefc + sdefr1 - rw

			XMVECTOR rw = XMVectorAdd(sdefr0 * w0, sdefr1 * w1);
			XMVECTOR r0 = XMVectorSubtract(XMVectorAdd(sdefc, sdefr0), rw);
			XMVECTOR r1 = XMVectorSubtract(XMVectorAdd(sdefc, sdefr1), rw);

				// cr0 = (sdefc + r0) * 0.5f
				// cr1 = (sdefc + r1) * 0.5f

			XMVECTOR cr0 = XMVectorAdd(sdefc, r0) * 0.5f;
			XMVECTOR cr1 = XMVectorAdd(sdefc, r1) * 0.5f;

			BoneNode* bone0 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[0]);
			BoneNode* bone1 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[1]);

			XMVECTOR q0 = XMQuaternionRotationMatrix(bone0->GetGlobalTransform());
			XMVECTOR q1 = XMQuaternionRotationMatrix(bone1->GetGlobalTransform());

			XMMATRIX m0 = XMMatrixMultiply(bone0->GetInitInverseTransform(), bone0->GetGlobalTransform());
			XMMATRIX m1 = XMMatrixMultiply(bone1->GetInitInverseTransform(), bone1->GetGlobalTransform());

			XMMATRIX rotation = XMMatrixRotationQuaternion(XMQuaternionSlerp(q0, q1, w1));

			position += morphPosition;

				// XMVector3Transform(position - sdefc, rotation) + XMVector3Transform(cr0, m0) * w0 + XMVector3Transform(cr1, m1) * w1

			XMVECTOR a = XMVector3Transform(XMVectorSubtract(position, sdefc), rotation);
			XMVECTOR b = XMVector3Transform(cr0, m0) * w0;
			XMVECTOR c = XMVector3Transform(cr1, m1) * w1;

			position = XMVectorAdd(XMVectorAdd(a, b), c);
			XMVECTOR normal = XMLoadFloat3(&currentVertexData.normal);
			normal = XMVector3Transform(normal, rotation);
			XMStoreFloat3(&_uploadVertices[i].normal, normal);
			break;
		}
		case PMXVertexWeight::QDEF:
		{
			BoneNode* bone0 = _nodeManager.GetBoneNodeByIndex(currentVertexData.boneIndices[0]);
			XMMATRIX m0 = XMMatrixMultiply(bone0->GetInitInverseTransform(), bone0->GetGlobalTransform());
			position += morphPosition;

			position = XMVector3Transform(position, m0);

			break;
		}
		default:
			break;
		}

		XMStoreFloat3(&_uploadVertices[i].position, position);

		const XMFLOAT4& morphUV = _morphManager.GetMorphUV(i);
		const XMFLOAT2& originalUV = _uploadVertices[i].uv;
		_uploadVertices[i].uv = XMFLOAT2(originalUV.x + morphUV.x, originalUV.y + morphUV.y);
	}
}

結果確認

今回は本当に長くなってしまいました。

それでは、ビルドして確認してみましょう。
Physics.gif

服や髪の毛がよく揺れています!
いいですね。

まとめ

今回も内容がかなり長くなってしまったようです。

実際には新しいクラスを追加して設定を行っただけで、物理計算はBullet Physicsが自動的に行ってくれるため、内容自体は難しくありません。

実際に試してみると、ポリゴンが引き裂かれるように見えることもありますが、この点は自分で修正方法を探してみてください。

次の記事からは再び描画に関連する内容が出てきます。

それでは次回お会いしましょう。

次回

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?