3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【UE4】Static MeshにBoxコリジョンをN個追加するエディタユーティリティを作った

Last updated at Posted at 2021-08-10

Spline Componentでエディットしたスプラインの一部に、Spline Mesh Componentを並べるものを作っている。
とどのつまり、これは要するに「みち」である!

image.png

さて:

  • 何やらSpline Meshが複数個並んでいるが、この記事の本題はそこにないのでいったんスルー。
    • 軽く解説すると、もとのメッシュの長さを (できる限り) 維持することを重視しており、一個のメッシュを長ーく引き延ばして配置するより、複数個のメッシュを細かく置くようにしてある。
    • ざっくり、(スプラインの長さ)÷(メッシュの長さ) 個のSpline Mesh Componentが置かれているということ。
    • こうすることで、テクスチャがびろ~んとヤな感じに伸びる問題を回避している。
  • 何やらSpline Componentが2本あるようだが、この記事の本題はそこにないので、これまたスルー。
    • 軽く解説すると、メインのスプラインを開始点からの距離Ds~距離Deまでで切り取った「サブスプライン(←造語)」の実験である。
    • 困ったことに Spline ComponentのGetTangentAt*()で取得できるタンジェントを、サブスプラインにそのまま割り当ててもうまくいかなかった。なので、(エンジンソースをみて) これを回避する計算を模索していたものである。(最終的にはうまくいった!)

この記事の本題はこう(↓)である。

  1. Procedural Mesh Componentで道路となるメッシュを作成した。(平べったい直方体)
  2. これをStatic Meshにコンバートして、Spline Meshにした。
  3. ところが、当然、コリジョンがない。どうしよう! (←これが本題)

というより、Complex Collisionは重くなるだろうから使いたくないのである。

Static Mesh Editorでの手動作業はこういう感じ

お馴染みの、Collisionメニュー > Add Box Simplified Collision である。

image.png

これ、Static Meshには十分なのだが、Spline Meshには不十分。
Add Box Simplified Collisionは「元のメッシュをきっちり包むBOX」を生成する。
この状態でSpline Meshに適用すると、コリジョンはこうなる。
※Show Collisionでコリジョンをワイヤフレーム表示してある。

image.png

分かりにくいかもしれないので、問題点をズームした画像を載せる。

image.png

要するに、コリジョンのメッシュは、見た目のメッシュよりも頂点が少ないので、Splineで曲げると元のメッシュから大きくずれるのである。
これの対処方法はこう(↓)。
お馴染みの、UCXで小さなBOXをたくさん並べる…を、DCCツール上ではなく、Static Mesh Editor上でやっているだけである。

image.png

上のスクショはStatic Mesh Editorのもの。緑色のワイヤフレームがSimple Collisionを表示したもの。
これは以下のような作業を行った結果である:

  • まずは、Collisionメニュー > Remove Collisionで、すべてのコリジョンデータを削除。
  • Add Simplified Box Collisionを4回実行。(今回は前後方向に4分割されているメッシュなので)
  • それぞれのSimplified Box Collisionがちょうど4分割になるように位置やサイズを入力。

これをSpline Meshで使うと、先ほどの問題点が解消されて、見た目とコリジョンが一致する。

image.png

この作業、何回もやるにはとても面倒くさい!

上の例は分割数が4回程度で済んでいるが、もっと細かい分割がしたかったり、ちょうど良い分割数をトライ&エラーしたかったり、手作業ではあまりやりたくない作業である。
これサクッと行うエディタユーティリティが欲しいところである。

ということで「Editor Scripting Utilities」

image.png

に入っている「Add Simple Collisions」を使ってみたのだが、

image.png

…とても、残念な結果が出た。
つまり「元のメッシュをきっちり包むBOX」が生成されるが、そのBOXの位置やサイズをスクリプトから弄る方法が提供されていないのである。

C++で作ってみた。

エディタソースをみて学んだあれこれ

  • Visual Studio でソース全体を「Add Simplified Box Collision」で検索すると、とっかかりが掴める。
    • 色々周りまわって、GenerateBoxAsSimpleCollision()という関数に出会えるはず。(GeomFitUtils.h, cpp)
  • もちろん、Editor Scripting UtilitiesプラグインつながりでAddSimpleCollisions()を探してもよい。

ということで、GenerateBoxAsSimpleCollision()を利用するか、カスタマイズするかすればよい。

自作ソースコード(C++)

残念ながら、上述のGenerateBoxAsSimpleCollision()は、Static Mesh EditorのCollisionメニュー > Add Simplified Box Collision以上の機能はない。つまり、元のメッシュをきっちり包むBOXしか生成できない。
(エンジンソースを転載することは、はばかられるので、お手元のソースコードをご確認ください)

なので、これを参考に自作してみた。皆さんの参考になれば幸いである。

もちろん、エディタモジュールを用意する必要がある。
普段のゲーム用モジュール上で組むと、後々困ることになるので注意。

こんな感じで使う

Editor Utility Widgetはこんな感じ:

image.png

  • Mesh AssetにStatic Meshを指定する。
  • Num Divisionに分割数を指定する。
  • Add Box Collisions for Spline Meshボタンで実行。

実行時の動作はこんな感じ:

image.png

細かいことはさておき、概要はこうである。

  • 関数Add Box Collisionsの引数は、上記Editor Utility Widgetで指定したStatic Meshアセットや分割数。
  • Sequence Then 0:
    • 作業をするStatic MeshのBoundsを取得して、X方向に分割するための細かいパラメータ群を算出。
  • Sequence Then 1:
    • 自作のユーティリティオブジェクトEditor Static Mesh Utility ObjectをConstruct Objectする。
    • 自作オブジェクトEditor Static Mesh Utility ObjectSet Static Meshで作業をするStatic Mesh Assetを指定。
  • Sequence Then 2:
    • 指定された分割数分のForループ
      • このForループは自作のマクロである。UE4標準のForループは第二引数にLast Indexをとるが、このマクロは第二引数にMax(つまりループ回数)を取る。
      • 個人的にForループはfor (i = 0; i < Max; ++i)とやりたい派なのである。
    • 自作オブジェクトEditor Static Mesh Utility ObjectAdd Box Simplified Collision()で、位置やサイズを指定しながら新規追加を行う。

C++ソースの要点

自作ユーティリティオブジェクトの全ソースコードは、後で載せる。
まずは、位置やサイズを指定しつつ、新規追加するコードから。

void UEditorStaticMeshUtilityObject::AddBoxSimplifiedCollision(FVector InCenter, FRotator InRotation, FVector InSize)
{
	// See GenerateBoxAsSimpleCollision() in GeomFitUtils.cpp
	// See UEditorStaticMeshLibrary::AddSimpleCollisionWithNotification() in EditorScriptingUtilities module.
	if (utils::IsValidMsg(MeshAsset, TEXT("AddBoxSimplifiedCollision")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		Helper.PreEdit(); // preprocesses
		{
			// main processes
			Helper.AddBox(InCenter, InRotation, InSize);
		}
		Helper.PostEdit(); // postprocesses
	}
}

要するに

  • 前処理 utils::BodySetupHelper::PreEdit()
  • 本題の処理 (ここでは位置やサイズを指定しつつ、BOXコリジョンを新規追加)
  • 後処理 utils::BodSetupHelper::PostEdit()

となる。ぶっちゃけ、ここをスタート地点にソースをたどってくださいませ!…ということで。

もちろん、Epicオリジナルの GenerateBoxAsSimpleCollision() もご参照ください。
GeomFitUtils.h, cpp。(要UnrealEdモジュール)

EditorStaticMeshUtilityObject.h

  • 関数ライブラリとしては作りにくかったのでUObjectを継承したユーティリティオブジェクトとした。
  • 最初に出てくるFEditorStaticMeshBoxCollision構造体は、Blueprintとのやり取りをするためのデータ。
    • Epicソースの内部ではFKBoxElem構造体が使われているのだが、Blueprintに公開されていないので、似て非なるものを自作。(あまり良くないが背に腹は代えられない)
  • 続く UEditorStaticMeshUtilityObjectが本題のオブジェクト。
    • UStaticMeshをメンバ変数に持ち、それをアレコレする関数群があるシンプルな構成。
EditorStaticMeshUtilityObject.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "EditorStaticMeshUtilityObject.generated.h"


// forward declarations
class UStaticMesh;


USTRUCT(BlueprintType)
struct FEditorStaticMeshBoxCollision
{
	GENERATED_BODY()

public:
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite) FVector Center;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) FRotator Rotation;
	UPROPERTY(EditAnywhere, BlueprintReadWrite) FVector Size;

	FEditorStaticMeshBoxCollision()
		: Center(FVector::ZeroVector), Rotation(FRotator::ZeroRotator), Size(FVector::ZeroVector)
	{}

	FEditorStaticMeshBoxCollision(const FVector& InCenter, const FRotator& InRotation, const FVector& InExtent)
		: Center(InCenter), Rotation(InRotation), Size(InExtent)
	{}
};


// Utility object for editing static mesh asset
UCLASS(Blueprintable)
class FORKHEADED_API UEditorStaticMeshUtilityObject : public UObject
{
	GENERATED_BODY()

public:

	// Set static mesh asset object.
	// This is first step to handle static mesh asset with this class object.
	UFUNCTION(BlueprintCallable, Category = "Editor Static Mesh Utility Object")
		void SetStaticMesh(UStaticMesh* InMeshAsset) { MeshAsset = InMeshAsset; }

	// Returns number of box simplified collision primitives.
	UFUNCTION(BlueprintPure, Category = "Editor Static Mesh Utility Object")
		int32 GetNumBoxSimplifiedCollisions() const;

	// Returns box simplified collision parameters of given index.
	UFUNCTION(BlueprintPure, Category = "Editor Static Mesh Utility Object")
		FEditorStaticMeshBoxCollision GetBoxSimplifiedCollision(int32 InIndex);

	// Returns all box simplified collision primitives.
	UFUNCTION(BlueprintCallable, Category = "Editor Static Mesh Utility Object")
		void GetAllBoxSimplifiedCollisions(TArray<FEditorStaticMeshBoxCollision>& OutBoxes);

	// Modifies existing box simplified collision parametes of given index.
	UFUNCTION(BlueprintCallable, Category = "Editor Static Mesh Utility Object")
		void SetBoxSimplifiedCollision(int32 InIndex, const FEditorStaticMeshBoxCollision& InNewData);

	// Add box simplified collision primitve with its parameters.
	// This is extended version of UEditorStaticMeshLibrary::AddSimpleCollisions() in EditorScriptingUtilities plugin.
	UFUNCTION(BlueprintCallable, Category = "Editor Static Mesh Utility Object")
		void AddBoxSimplifiedCollision(FVector InCenter, FRotator InRotation, FVector InSize);

public:

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
		UStaticMesh* MeshAsset = nullptr;
};

EDitorStaticMeshUtilityObject.cpp

  • 冒頭に出てくるnamespace utilsは、このcppソースファイル内でだけ使いたいユーティリティ関数やクラスが入っている。
    • どちらかというと、これがメイン。
    • あと、私は本体のオブジェクト UEditorStaticMeshUtilityObjectにprivate関数をユーティリティとして足すよりは、こうやってcppソースファイル内だけで完結する組み方をするのが好み。
    • あまりヘッダファイルを汚したくない、という意図。
  • 上述した通り、AddBoxSimplifiedCollision()関数がポイント。
    • このソースファイルの一番下にある関数。
    • 具体的に何をやっているかは、ここを起点に探してください。
EditorStaticMeshUtilityObject.cpp
#include "EditorStaticMeshUtilityObject.h"
#include "Engine/StaticMesh.h" // UStaticMesh
#include "Editor/UnrealEd/Private/GeomFitUtils.h" // RefreshCollisionChange()


DEFINE_LOG_CATEGORY_STATIC(LogEditorStaticMesh, Log, All);


namespace utils
{
	static bool IsValidMsg(const UStaticMesh* InAsset, FString InFuncName)
	{
		if (InAsset == nullptr)
		{
			UE_LOG(LogEditorStaticMesh, Warning, TEXT("%s() You need to set a static mesh asset by SetStaticMesh()"), *InFuncName);
			return false;
		}
		else
		{
			return true;
		}
	}


	// See GenerateBoxAsSimpleCollision() in GeomFitUtils.cpp
	class BodySetupHelper
	{
	private:
		UStaticMesh* MeshAsset = nullptr;
		UBodySetup* BodySetup = nullptr;

	public:
		BodySetupHelper(UStaticMesh* InMeshAsset) 
			: MeshAsset(InMeshAsset), BodySetup(InMeshAsset->BodySetup) 
		{}

		FORCEINLINE int32 GetNumBoxes() const
		{
			TArray<FKBoxElem>* BoxArray = GetBoxArrayPtr();
			return (BoxArray) ? BoxArray->Num() : 0;
		}

		FORCEINLINE const FKBoxElem* GetBox(int32 InIndex) const
		{
			TArray<FKBoxElem>* BoxArray = GetBoxArrayPtr();
			return (BoxArray && BoxArray->IsValidIndex(InIndex)) ? (BoxArray->GetData() + InIndex) : nullptr;
		}

		void SetBox(int32 InIndex, const FVector& InCenter, const FRotator& InRotation, const FVector& InSize)
		{
			TArray<FKBoxElem>* BoxArray = GetBoxArrayPtr();
			if (BoxArray && BoxArray->IsValidIndex(InIndex))
			{
				FKBoxElem* Box = BoxArray->GetData() + InIndex;
				Box->Center = InCenter;
				Box->Rotation = InRotation;
				Box->X = InSize.X;
				Box->Y = InSize.Y;
				Box->Z = InSize.Z;
			}
		}

		void AddBox(const FVector& InCenter, const FRotator& InRotation, const FVector& InSize)
		{
			TArray<FKBoxElem>* BoxArray = GetBoxArrayPtr();
			if (BoxArray)
			{
				FKBoxElem BoxElem;
				BoxElem.Center = InCenter;
				BoxElem.Rotation = InRotation;
				BoxElem.X = InSize.X;
				BoxElem.Y = InSize.Y;
				BoxElem.Z = InSize.Z;
				BoxArray->Add(BoxElem);
			}
		}

		// Performs preprocessing before changing simplified collision data of the static mesh.
		void PreEdit()
		{
			if (BodySetup)
			{
				// Notify the object will be modified.  
				// If recording into the transaction buffer (undo/redo), saves the copy into the buffer
				// and marks the package as needing to be saved.
				BodySetup->Modify();

				// Clear the cache (PIE may have created some data), create new GUID
				BodySetup->InvalidatePhysicsData();
			}
		}

		// Performs postprocessing after changing simplified collision data of the static mesh.
		void PostEdit()
		{
			RefreshCollisionChange(/*UStaticMesh&=*/*MeshAsset); // refresh collision change back to static mesh components (GeomFitUtils.h, .cpp)
			MeshAsset->MarkPackageDirty(); // mark static mesh as dirty, to help make sure it gets saved.
			MeshAsset->bCustomizedCollision = true; // mark the static mesh for collision customization.
			MeshAsset->PostEditChange(); // request re-building of mesh with new collision shapes.
		}

	private:
		FORCEINLINE TArray<FKBoxElem>* GetBoxArrayPtr() const
		{
			return (BodySetup) ? &(BodySetup->AggGeom.BoxElems) : nullptr;
		}

	}; // class BodySetup

} // namespace utils


int32 UEditorStaticMeshUtilityObject::GetNumBoxSimplifiedCollisions() const
{
	if (utils::IsValidMsg(MeshAsset, TEXT("GetNumBoxSimplifiedCollisions()")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		return Helper.GetNumBoxes();
	}
	else
	{
		return 0;
	}
}


FEditorStaticMeshBoxCollision UEditorStaticMeshUtilityObject::GetBoxSimplifiedCollision(int32 InIndex)
{
	if (utils::IsValidMsg(MeshAsset, TEXT("GetBoxSimplifiedCollision")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		const FKBoxElem* Box = Helper.GetBox(InIndex);
		if (Box)
		{
			return FEditorStaticMeshBoxCollision(Box->Center, Box->Rotation, FVector(Box->X, Box->Y, Box->Z));
		}
	}

	UE_LOG(LogEditorStaticMesh, Warning, TEXT("GetBoxSimplifiedCollision() failed."));
	static FEditorStaticMeshBoxCollision Dummy;
	return Dummy;
}


void UEditorStaticMeshUtilityObject::GetAllBoxSimplifiedCollisions(TArray<FEditorStaticMeshBoxCollision>& OutBoxes)
{
	OutBoxes.Empty();

	if (utils::IsValidMsg(MeshAsset, TEXT("GetAllBoxSimplifiedCollisions")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		const int32 NumBoxes = Helper.GetNumBoxes();
		if (NumBoxes > 0)
		{
			OutBoxes.SetNum(NumBoxes);

			for (int32 i = 0; i < NumBoxes; ++i)
			{
				const FKBoxElem* Box = Helper.GetBox(i);
				if (Box)
				{
					FEditorStaticMeshBoxCollision& OutBox = OutBoxes[i];
					OutBox.Center = Box->Center;
					OutBox.Rotation = Box->Rotation;
					OutBox.Size = FVector(Box->X, Box->Y, Box->Z);
				}
			}
		}
	}
}


void UEditorStaticMeshUtilityObject::SetBoxSimplifiedCollision(int32 InIndex, const FEditorStaticMeshBoxCollision& InNewData)
{
	// See GenerateBoxAsSimpleCollision() in GeomFitUtils.cpp
	// See UEditorStaticMeshLibrary::AddSimpleCollisionWithNotification() in EditorScriptingUtilities module.
	if (utils::IsValidMsg(MeshAsset, TEXT("SetBoxSimplifiedCollision")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		Helper.PreEdit(); // preprocesses
		{
			// main processes
			Helper.SetBox(InIndex, InNewData.Center, InNewData.Rotation, InNewData.Size);
		}		
		Helper.PostEdit(); // postprocesses
	}
}


void UEditorStaticMeshUtilityObject::AddBoxSimplifiedCollision(FVector InCenter, FRotator InRotation, FVector InSize)
{
	// See GenerateBoxAsSimpleCollision() in GeomFitUtils.cpp
	// See UEditorStaticMeshLibrary::AddSimpleCollisionWithNotification() in EditorScriptingUtilities module.
	if (utils::IsValidMsg(MeshAsset, TEXT("AddBoxSimplifiedCollision")))
	{
		utils::BodySetupHelper Helper(MeshAsset);
		Helper.PreEdit(); // preprocesses
		{
			// main processes
			Helper.AddBox(InCenter, InRotation, InSize);
		}
		Helper.PostEdit(); // postprocesses
	}
}

以上です。
UE4 Editor側の流儀については、まだあまり自信がないので、おかしいところがあればご指摘ください。

3
1
0

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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?