はじめに
こちらの記事に記載がある通り、UE5からはMaterialのTessellationが削除されたので、代替手段としてGeometryScriptが利用可能です。
本記事ではSkeletal MeshのTessellationを考えます。
ゲーム向け(ランタイム)ではなく主に映像向けで、「Shot0001ではインポートしたままでいいけど、Shot0002ではTessellationしたキャラを表示したい」というニーズへの対応がベースとなっています。
環境
- Windows 11
- UE 5.6
方針
- Tessellationを適用しても、インポートしたSkeletal Meshは変更状態にしたくない
- 各Shot(レベル毎)でTessellationの度合いを変更できるようにしたい
GeometryScriptをそのままやると、インポートしたSkeletal Meshが変更状態となります。
なので、ActorでSkeletal Meshを複製して、その複製したものに対してTessellationを適用するようにします。 専用のDataAssetでSkeletal Meshを複製して、その複製したものに対してTessellationを適用するようにします。
最初はActorでSkeletal Meshを複製する手法を取っていましたが、
複製(DuplicateObject)したUObjectをUPROPERTYありで保持していると
PIE、MRQ実行時などでクラッシュするので方針を変更しました。
実装・設定
専用のDataAsset(UTessellatedSkeletalMeshDataAsset)
DataAsset(UTessellatedSkeletalMeshDataAsset)の.hと.cppです。
「Skeletal Meshを複製」はDuplicatedSkeletalMeshに該当します。
「Tessellationを適用したもの」はTessellatedSkeletalMeshesに該当します。
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "TessellatedSkeletalMeshDataAsset.generated.h"
UCLASS()
class TESSELLATIONTEST_API UTessellatedSkeletalMeshDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, CallInEditor, meta=(DisplayName="Generate Tessellated Skeletal Meshes"))
void GenerateTessellatedSkeletalMeshes();
TArray<TObjectPtr<USkeletalMesh>> GetTessellatedSkeletalMeshes() const { return TessellatedSkeletalMeshes; }
private:
UPROPERTY(EditDefaultsOnly, meta=(AllowPrivateAccess = "true"))
TObjectPtr<class USkeletalMesh> SkeletalMesh;
UPROPERTY(EditDefaultsOnly, meta=(AllowPrivateAccess = "true", UIMin = 1, ClampMin = 1, ClampMax = 10))
int MaxTessellationLevel = 1;
UPROPERTY()
TArray<TObjectPtr<USkeletalMesh>> TessellatedSkeletalMeshes;
};
#include "TessellatedSkeletalMeshDataAsset.h"
#include "Components/DynamicMeshComponent.h"
#include "GeometryScript/MeshAssetFunctions.h"
#include "GeometryScript/MeshSubdivideFunctions.h"
void UTessellatedSkeletalMeshDataAsset::GenerateTessellatedSkeletalMeshes()
{
if (SkeletalMesh == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("GenerateTessellatedSkeletalMeshes: SkeletalMesh is null."));
return;
}
if (MaxTessellationLevel <= 0)
{
// UPROPERTYで制限しているけど一応
UE_LOG(LogTemp, Warning, TEXT("GenerateTessellatedSkeletalMeshes: MaxTessellationLevel is less than 1."));
return;
}
// 以前に生成したメッシュがあれば削除
if (!TessellatedSkeletalMeshes.IsEmpty())
{
// Empty()だけだとアセットサイズが増えていく(適切に解放できていない)ので、その対策
for (USkeletalMesh* Mesh : TessellatedSkeletalMeshes)
{
if (Mesh != nullptr)
{
// 既存のメッシュを削除
Mesh->MarkAsGarbage();
Mesh = nullptr;
}
}
TessellatedSkeletalMeshes.Empty();
// GCの保険
FPlatformProcess::Sleep(0.1f);
}
// index0 = 元モデルをそのまま
TessellatedSkeletalMeshes.Add(DuplicateObject<USkeletalMesh>(SkeletalMesh, this));
// Tessellationを適用したメッシュを生成
for (int32 TessellationLevel = 1; TessellationLevel <= MaxTessellationLevel; ++TessellationLevel)
{
TObjectPtr<UDynamicMesh> DynamicMesh = NewObject<UDynamicMesh>();
EGeometryScriptOutcomePins Outcome;
UGeometryScriptLibrary_StaticMeshFunctions::CopyMeshFromSkeletalMesh(
SkeletalMesh,
DynamicMesh,
FGeometryScriptCopyMeshFromAssetOptions {}, // デフォルト設定のまま
FGeometryScriptMeshReadLOD {}, // デフォルト設定のまま
Outcome
);
if (Outcome == EGeometryScriptOutcomePins::Success)
{
UGeometryScriptLibrary_MeshSubdivideFunctions::ApplyPNTessellation(
DynamicMesh,
FGeometryScriptPNTessellateOptions {}, // デフォルト設定のまま
TessellationLevel);
TObjectPtr<USkeletalMesh> TessellatedSkeletalMesh = DuplicateObject<USkeletalMesh>(SkeletalMesh, this);
const FString PathName = TessellatedSkeletalMesh->GetPathName();
UE_LOG(LogTemp, Log, TEXT("GenerateTessellatedSkeletalMeshes: TessellatedSkeletalMesh's PathName: %s."), *PathName);
UGeometryScriptLibrary_StaticMeshFunctions::CopyMeshToSkeletalMesh(
DynamicMesh,
TessellatedSkeletalMesh,
FGeometryScriptCopyMeshToAssetOptions {}, // デフォルト設定のまま
FGeometryScriptMeshWriteLOD {}, // デフォルト設定のまま
Outcome);
if (Outcome == EGeometryScriptOutcomePins::Success)
{
TessellatedSkeletalMeshes.Add(TessellatedSkeletalMesh);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("GenerateTessellatedSkeletalMeshes: CopyMeshToSkeletalMesh() is failed. TessellationLevel: %d"), TessellationLevel);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("GenerateTessellatedSkeletalMeshes: CopyMeshFromSkeletalMesh() is failed."));
}
}
}
専用のDataAssetを持つActor (ATessellatableSkeletalMeshActor)
DataAsset(UTessellatedSkeletalMeshDataAsset)を持つ
Actor(ATessellatableSkeletalMeshActor)の.hと.cppです。
ASkeletalMeshActorを継承していますが、深い意味は特にないです。
#pragma once
#include "CoreMinimal.h"
#include "TessellatedSkeletalMeshDataAsset.h"
#include "Animation/SkeletalMeshActor.h"
#include "TessellatableSkeletalMeshActor.generated.h"
UCLASS()
class TESSELLATIONTEST_API ATessellatableSkeletalMeshActor : public ASkeletalMeshActor
{
GENERATED_BODY()
ATessellatableSkeletalMeshActor(const FObjectInitializer& ObjectInitializer);
public:
/** UObject interface */
#if WITH_EDITOR
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
/** end UObject interface */
protected:
void UpdateSkeletalMesh();
private:
UPROPERTY(Category=TessellatableSkeletalMeshActor, EditDefaultsOnly, meta=(AllowPrivateAccess = "true"))
TObjectPtr<UTessellatedSkeletalMeshDataAsset> TessellatedSkeletalMeshDataAsset;
UPROPERTY(Category=TessellatableSkeletalMeshActor, EditAnywhere, meta=(AllowPrivateAccess = "true"))
int TessellationLevel = 0;
};
#include "TessellatableSkeletalMeshActor.h"
ATessellatableSkeletalMeshActor::ATessellatableSkeletalMeshActor(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
#if WITH_EDITOR
void ATessellatableSkeletalMeshActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
const FName PropertyName = (PropertyChangedEvent.Property != NULL) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
if (PropertyName == GET_MEMBER_NAME_CHECKED(ATessellatableSkeletalMeshActor, TessellatedSkeletalMeshDataAsset))
{
TessellationLevel = 0; // モデルが変更されたものとして、Tessellationされていない状態で表示する
UpdateSkeletalMesh();
}
else if (PropertyName == GET_MEMBER_NAME_CHECKED(ATessellatableSkeletalMeshActor, TessellationLevel))
{
UpdateSkeletalMesh();
}
}
#endif
void ATessellatableSkeletalMeshActor::UpdateSkeletalMesh()
{
if (TessellatedSkeletalMeshDataAsset != nullptr)
{
const TArray<TObjectPtr<USkeletalMesh>> SkeletalMeshes = TessellatedSkeletalMeshDataAsset->GetTessellatedSkeletalMeshes();
if (SkeletalMeshes.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("UpdateSkeletalMesh: SkeletalMeshes is empty."));
return;
}
TessellationLevel = FMath::Clamp(TessellationLevel, 0, SkeletalMeshes.Num() - 1);
GetSkeletalMeshComponent()->SetSkeletalMesh(SkeletalMeshes[TessellationLevel]);
}
else
{
TessellationLevel = 0;
GetSkeletalMeshComponent()->SetSkeletalMesh(nullptr);
}
}
UTessellatedSkeletalMeshDataAssetを作成&Tessellationを生成
Content BrowserでUTessellatedSkeletalMeshDataAssetを作成します。

作成したDataAssetを開いて、
Tessellationを生成するメッシュ(Skeletal Mesh)、
Tessellationの最大レベル(Max Tessellation Level)
を設定後、ボタン(Generate Tessellated Skeletal Meshes)を押すとしばらく処理が走り、保存します。

ATessellatableSkeletalMeshActor継承したBPを作成
Skeletal Mesh毎にATessellatableSkeletalMeshActorを継承したBPを作成します。
TessellatedSkeletalMeshDataAssetに前で作成したDataAssetを設定します。
各レベル毎の設定
前で作成したBPをレベルに配置して、Tessellation Levelを任意に変更(値が大きいほどより詳細度が上がる)すればTessellationが適用されたSkeletal Meshが表示されます。
当然、アニメーションも動きます。
設定が完了したらレベルを保存します。
留意事項
実運用を想定すると色々課題は残ります。
- Skeletal Meshを再インポートするときはレベル毎を更新する必要がある。
UImportSubsystem::OnAssetReimportをフックしてごにょごにょする。 - ルックデブ中だとSkeletal Meshのマテリアル更新が頻繁にあるので、その更新に合わせてDataAssetの更新が
ダルい面倒。 - そもそものGeometryScriptのTessellationを通したときにレンダリングで破綻した表示にならないかは要確認。オプション設定で回避可能?
ただ、自己満はしたので一旦は終わりで、、、
他にも研究したい内容が色々あるので。。。
「ソースが欲しい」という方がいれば、Googleドライブなどで共有します。
この記事通りにすれば再現は可能なはずです。

