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?

Tessellation可能なSkeletal Mesh?

0
Last updated at Posted at 2025-08-04

はじめに

こちらの記事に記載がある通り、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に該当します。

TessellatedSkeletalMeshDataAsset.h

#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;
};

TessellatedSkeletalMeshDataAsset.cpp

#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を継承していますが、深い意味は特にないです。

TessellatableSkeletalMeshActor.h

#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;
};
TessellatableSkeletalMeshActor.cpp

#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を作成します。
2025-08-04_21h58_56.png

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

ATessellatableSkeletalMeshActor継承したBPを作成

Skeletal Mesh毎にATessellatableSkeletalMeshActorを継承したBPを作成します。
TessellatedSkeletalMeshDataAssetに前で作成したDataAssetを設定します。

image.png

各レベル毎の設定

前で作成したBPをレベルに配置して、Tessellation Levelを任意に変更(値が大きいほどより詳細度が上がる)すればTessellationが適用されたSkeletal Meshが表示されます。

image.png

当然、アニメーションも動きます。

設定が完了したらレベルを保存します。

留意事項

実運用を想定すると色々課題は残ります。

  • Skeletal Meshを再インポートするときはレベル毎を更新する必要がある。UImportSubsystem::OnAssetReimportをフックしてごにょごにょする。
  • ルックデブ中だとSkeletal Meshのマテリアル更新が頻繁にあるので、その更新に合わせてDataAssetの更新がダルい面倒。
  • そもそものGeometryScriptのTessellationを通したときにレンダリングで破綻した表示にならないかは要確認。オプション設定で回避可能?

ただ、自己満はしたので一旦は終わりで、、、
他にも研究したい内容が色々あるので。。。

「ソースが欲しい」という方がいれば、Googleドライブなどで共有します。
この記事通りにすれば再現は可能なはずです。

0
0
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
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?