モジュラー キャラクターで作業する

複数のスケルタル メッシュで構成されるモジュラー キャラクターを作成するさまざまな方法について説明します。

Choose your operating system:

Windows

macOS

Linux

ModularBanner.png

プレーヤーによるキャラクターのカスタマイズ、さまざまな頭部やボディーのタイプ、クロスやその他のオプションなどの異なるパーツの入れ替えを可能にするシステムを構築する際は、キャラクターをモジュラーで作成することをお勧めします。キャラクター全体に対して 1 つのスケルタル メッシュをインポートするのではなく、スケルタル メッシュを胴、脚、頭部など複数のセクションに分けて、エンジンにインポートします。次に、このページに記載されているいくつかの方法で、これらを組み立ててアニメーションすることができます。これにより、さまざまなキャラクターの生成において柔軟性が高まるだけでなく、パフォーマンスも向上します。

Master Pose Component

Master Pose Component とは、 Skinned Mesh Component オブジェクト を、マスターとみなされる別の Skinned Mesh Component オブジェクトの子として設定することができる、ブループリントで呼び出し可能な関数です。例えば、Torso (胴) をマスター ポーズ コンポーネントとして定義し、その Torso にアニメーションを割り当てて、そのアニメーションに従う足、脚、手および頭部を子として追加します。

バックグラウンドでは、これらの子は Bone Transform Buffer をまったく使用せず、アニメーションが設定されている場合であってもアニメーションを実行することはありません。レンダリング時には Torso の Bone Transform Buffer のみが使用されるため、非常に軽量なアタッチメント システムとなります。アニメーションを実行する必要がある唯一のコンポーネントは Torso であり、アタッチされたすべてのコンポーネントは Torso の Bone Transform を使用します。下の図は、アニメーションを Torso に割り当てた設定の例です。上の図では、 Construction Script をブループリント内で使用して、Torso スケルタル メッシュを Master Bone Component として設定し、モジュール キャラクターのその他の部分を子として設定します。

SetMasterPoseComponentImage.png

上の図では、 Construction Script をブループリント内で使用して、Torso スケルタル メッシュを Master Bone Component として設定し、モジュール キャラクターのその他の部分を子として設定します。

Set Master Pose Component 関数には、 Force Update と呼ばれる Boolean タイプの 2 番目の引数があります。Force Update を false に設定すると、入力マスター コンポーネントとランタイム情報が同じである場合はすべてのランタイム情報の更新がスキップされます (有効な場合は、ランタイム情報が強制的に更新されます)。これがあてはまるのはシリアライズ可能な登録時のみで、すべてのランタイム データを更新する必要があります。

キャラクターの各部分は、別の スケルタル メッシュ コンポーネント との交換が可能なスケルタル メッシュです。下の画像では、Torso と Feet の表示を切り替えました (これらを、同じスケルタル階層に従う異なるスケルタル メッシュと交換することも可能です)。

HiddenComponents.png

ただし、 マスター ポーズ コンポーネント を使用する際は、マスター ポーズ コンポーネントによりゲーム スレッド負荷は軽減されますが、レンダリング負荷は軽減されないことを考慮する必要があります。同じ数のコンポーネントを個別にレンダリングすることに変わりはありませんが、コンポーネントごとの追加のセクションによってより多くの描画呼び出しが生じることに留意してください。

また、Master Bone の子は完全に一致する構造を持つサブセットである必要があるという制限があるため、その他の追加のジョイントを含めたり、ジョイントをスキップしたりすることはできません。追加のジョイントについては Bone Buffer データは存在しないため、参照ポーズを使用してレンダリングされます。また、子ではその他のアニメーションや物理を実行することはできません。

Copy Pose From Mesh

Copy Pose From Mesh は 子の Animation ブループリント 上で使用できる AnimGraph ノードです。どの Skeletal Mesh Component からもアニメーションポーズをコピーすることができます。Copy Pose From Mesh は一致するボーンのみコピーし、その他すべては参照ポーズを使用します。ただし、コピーされたトランスフォームに加えてアニメーションをプレイすることができます。以下が説明です。

クリックしてフルサイズで表示

Copy Pose From Mesh を使用する際は、コピー元のスケルタル メッシュ コンポーネントがティック済みであることを確認することを推奨します。ティックしていない場合、最後のフレームのアニメーションをコピーすることになります (Body からコピーしているが、Head は子である場合など)。Body がティック済みであることを確実にするには、Head を Body にアタッチします。これで、子の前に親が最初にティックします。

この関係性はコードでも設定することができます。前提条件として設定することで、現在のコンポーネントの前にこれらがティックされることを確実にすることができます。詳細については、 「ティックの依存関係」 ページを参照してください。

Copy Pose From Mesh を使用する際に考慮すべき事項として、Copy Pose From Mesh ではそれぞれの子でアニメーションを実行するため、マスター ポーズ コンポーネントより負荷が高くなることが挙げられます。さらに、子に対して物理を使用する場合は、代わりに Rigid Body または AnimDynamics スケルタル制御ノードを使用することを推奨します。

アニメーション エディタでアニメーションをプレビューする際は、Copy Pose From Mesh を自動的に使用する追加のメッシュを割り当てることができます。キャラクターのコンポーネントなど、共にアニメートされる関連スケルタル メッシュのコレクションを構築する際に使用可能な、カスタム プレビュー メッシュ コレクション を作成することもできます。次のビデオでは、異なるスケルタル メッシュを変更してプレビューに割り当てる方法を示し、キャラクターにさまざまな頭部を適用できるようにしています。

Skeletal Mesh Merge

ランタイム時には、 FSkeletalMeshMerge を含むコードを介して複数のスケルタル メッシュを 1 つにマージすることができます。スケルタル メッシュの作成にかかる初期負荷は高くなりますが、複数のメッシュではなく 1 つのスケルタル メッシュのみをレンダリングすることになるため、レンダリング負荷は低くなります。例えば、3 つのコンポーネント (頭部、ボディ、脚) で構成されるキャラクターが画面上に 50 体ある場合、これは 50 件の描画呼び出し となります。スケルタル メッシュをマージしないと、それぞれのコンポーネントでの描画呼び出しにより各キャラクターで 3 回の呼び出しが行われるため、合計で 150 件の描画呼び出し が生じます。

FSkeletalMeshMerge の使用時には、マージされたメッシュでは、アニメートするすべてのジョイントを含み、設定されているスケルトンのみを使用するため、メインの「ボディ」にはすべてのアニメーションが含まれている必要があります。特定のボディ部分に追加のジョイントがある場合も、ボディにすべてのアニメーションを含める必要があります。その他の考慮事項として、マージされたメッシュでは 1 つのアニメーションのみを実行できることと、マージされたメッシュへのモーフ ターゲットの移行はサポートされていないことが挙げられます。ただし、 FSkeletalMeshMerge::GenerateLODModel の場合、スケルタル メッシュがあれば、ベース メッシュとモーフの間の FMorphTargetDelta を計算することで、モーフ ターゲットを作成することができます。

さらに、 FSkeletalMeshMerge を使用する際は、ほとんどの場合コンテンツを最初から特定の方法で構築する必要があります。テクスチャを切り取って貼り合わせて新しいものを作成し、キャラクター全体を 1 つのセクションとしてレンダリングできるようにするために、1 つの共通マテリアルを作成して、テクスチャのアトラスで決定 (ブーツやグローブが適用される領域など) することをお勧めします。

メッシュのマージ例

下記の例では、メッシュ マージ コードを使用して、複数のスケルタル メッシュをランタイム時に組み立てます。

Individual_Meshes.png

上の図では、ランタイム時に 1 つのスケルタル メッシュに結合させる複数のスケルタル メッシュが示されています。この例では、 Mesh Merge と呼ばれる、コードを介してブループリントで呼び出し可能な関数を作成します。この関数により、マージするメッシュを定義することができます。まず最初に、ブループリントからの関数の呼び出しを可能にする Blueprint Function Library に基づいて C++ クラスを作成し、「 MeshMergeFunctionLibrary 」と名前を付けます。

Blueprint_FunctionLibrary.png

以下には、 ヘッダ および ソース ファイル内で使用できる一連のサンプル コードが示されています。

.h code sample

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "UObject/NoExportTypes.h"
#include "MeshMergeFunctionLibrary.generated.h"
/**
* Blueprint equivalent of FSkeleMeshMergeSectionMapping
* Info to map all the sections from a single source skeletal mesh to
* a final section entry in the merged skeletal mesh.
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeSectionMapping_BP
{
    GENERATED_BODY()
        /** Indices to final section entries of the merged skeletal mesh */
        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
        TArray < int32 > SectionIDs;
};
/**
* Used to wrap a set of UV Transforms for one mesh.
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeUVTransform
{
    GENERATED_BODY()
        /** A list of how UVs should be transformed on a given mesh, where index represents a specific UV channel. */
        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
        TArray < FTransform > UVTransforms;
};
/**
* Blueprint equivalent of FSkelMeshMergeUVTransforms
* Info to map all the sections about how to transform their UVs
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeUVTransformMapping
{
    GENERATED_BODY()
        /** For each UV channel on each mesh, how the UVS should be transformed. */
        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
        TArray < FSkelMeshMergeUVTransform > UVTransformsPerMesh;
};
/**
* Struct containing all parameters used to perform a Skeletal Mesh merge.
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkeletalMeshMergeParams
{
    GENERATED_BODY()
        FSkeletalMeshMergeParams()
    {
        StripTopLODS = 0;
        bNeedsCpuAccess = false;
        bSkeletonBefore = false;
        Skeleton = nullptr;
    }
    // An optional array to map sections from the source meshes to merged section entries
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        TArray < FSkelMeshMergeSectionMapping_BP > MeshSectionMappings;
    // An optional array to transform the UVs in each mesh
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        TArray < FSkelMeshMergeUVTransformMapping > UVTransformsPerMesh;
    // The list of skeletal meshes to merge.
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        TArray < USkeletalMesh* > MeshesToMerge;
    // The number of high LODs to remove from input meshes
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        int32 StripTopLODS;
    // Whether or not the resulting mesh needs to be accessed by the CPU for any reason (e.g. for spawning particle effects).
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        uint32 bNeedsCpuAccess : 1;
    // Update skeleton before merge. Otherwise, update after.
    // Skeleton must also be provided.
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
        uint32 bSkeletonBefore : 1;
    // Skeleton that will be used for the merged mesh.
    // Leave empty if the generated skeleton is OK.
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
        class USkeleton* Skeleton;
};
/**
*
*/
UCLASS()
class PROJECTNAME_API UMeshMergeFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:
    /**
    * Merges the given meshes into a single mesh.
    * @return The merged mesh (will be invalid if the merge failed).
    */
    UFUNCTION(BlueprintCallable, Category = "Mesh Merge", meta = (UnsafeDuringActorConstruction = "true"))
        static class USkeletalMesh* MergeMeshes(const FSkeletalMeshMergeParams& Params);
};

ヘッダー内ですべての PROJECTNAME_API リファレンスを実際のプロジェクト名に変更する必要があります。たとえば、プロジェクト名が "MyProject" であれば、コード作業の際はこれらすべてのインスタンスで MYPROJECT_API を使用する必要があります。

.cpp code sample

// Fill out your copyright notice in the Description page of Project Settings.
#include "MeshMergeFunctionLibrary.h"
#include "SkeletalMeshMerge.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/Skeleton.h"
static void ToMergeParams(const TArray<FSkelMeshMergeSectionMapping_BP>& InSectionMappings, TArray<FSkelMeshMergeSectionMapping>& OutSectionMappings)
{
    if (InSectionMappings.Num() > 0)
    {
        OutSectionMappings.AddUninitialized(InSectionMappings.Num());
        for (int32 i = 0; i < InSectionMappings.Num(); ++i)
        {
            OutSectionMappings[i].SectionIDs = InSectionMappings[i].SectionIDs;
        }
    }
};
static void ToMergeParams(const TArray<FSkelMeshMergeUVTransformMapping>& InUVTransformsPerMesh, TArray<FSkelMeshMergeUVTransforms>& OutUVTransformsPerMesh)
{
    if (InUVTransformsPerMesh.Num() > 0)
    {
        OutUVTransformsPerMesh.Empty();
        OutUVTransformsPerMesh.AddUninitialized(InUVTransformsPerMesh.Num());
        for (int32 i = 0; i < InUVTransformsPerMesh.Num(); ++i)
        {
            TArray<TArray<FTransform>>& OutUVTransforms = OutUVTransformsPerMesh[i].UVTransformsPerMesh;
            const TArray<FSkelMeshMergeUVTransform>& InUVTransforms = InUVTransformsPerMesh[i].UVTransformsPerMesh;
            if (InUVTransforms.Num() > 0)
            {
                OutUVTransforms.Empty();
                OutUVTransforms.AddUninitialized(InUVTransforms.Num());
                for (int32 j = 0; j < InUVTransforms.Num(); j++)
                {
                    OutUVTransforms[i] = InUVTransforms[i].UVTransforms;
                }
            }
        }
    }
};
USkeletalMesh* UMeshMergeFunctionLibrary::MergeMeshes(const FSkeletalMeshMergeParams& Params)
{
    TArray<USkeletalMesh*> MeshesToMergeCopy = Params.MeshesToMerge;
    MeshesToMergeCopy.RemoveAll([](USkeletalMesh* InMesh)
    {
        return InMesh == nullptr;
    });
    if (MeshesToMergeCopy.Num() <= 1)
    {
        UE_LOG(LogTemp, Warning, TEXT("Must provide multiple valid Skeletal Meshes in order to perform a merge."));
        return nullptr;
    }
    EMeshBufferAccess BufferAccess = Params.bNeedsCpuAccess ?
        EMeshBufferAccess::ForceCPUAndGPU :
        EMeshBufferAccess::Default;
    TArray<FSkelMeshMergeSectionMapping> SectionMappings;
    TArray<FSkelMeshMergeUVTransforms> UvTransforms;
    ToMergeParams(Params.MeshSectionMappings, SectionMappings);
    ToMergeParams(Params.UVTransformsPerMesh, UvTransforms);
    bool bRunDuplicateCheck = false;
    USkeletalMesh* BaseMesh = NewObject<USkeletalMesh>();
    if (Params.Skeleton && Params.bSkeletonBefore)
    {
        BaseMesh->Skeleton = Params.Skeleton;
        bRunDuplicateCheck = true;
        for (USkeletalMeshSocket* Socket :BaseMesh->GetMeshOnlySocketList())
        {
            if (Socket)
            {
                UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        for (USkeletalMeshSocket* Socket :BaseMesh->Skeleton->Sockets)
        {
            if (Socket)
            {
                UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
    }
    FSkeletalMeshMerge Merger(BaseMesh, MeshesToMergeCopy, SectionMappings, Params.StripTopLODS, BufferAccess, UvTransforms.GetData());
    if (!Merger.DoMerge())
    {
        UE_LOG(LogTemp, Warning, TEXT("Merge failed!"));
        return nullptr;
    }
    if (Params.Skeleton && !Params.bSkeletonBefore)
    {
        BaseMesh->Skeleton = Params.Skeleton;
    }
    if (bRunDuplicateCheck)
    {
        TArray<FName> SkelMeshSockets;
        TArray<FName> SkelSockets;
        for (USkeletalMeshSocket* Socket :BaseMesh->GetMeshOnlySocketList())
        {
            if (Socket)
            {
                SkelMeshSockets.Add(Socket->GetFName());
                UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        for (USkeletalMeshSocket* Socket :BaseMesh->Skeleton->Sockets)
        {
            if (Socket)
            {
                SkelSockets.Add(Socket->GetFName());
                UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
            }
        }
        TSet<FName> UniqueSkelMeshSockets;
        TSet<FName> UniqueSkelSockets;
        UniqueSkelMeshSockets.Append(SkelMeshSockets);
        UniqueSkelSockets.Append(SkelSockets);
        int32 Total = SkelSockets.Num() + SkelMeshSockets.Num();
        int32 UniqueTotal = UniqueSkelMeshSockets.Num() + UniqueSkelSockets.Num();
        UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), SkelMeshSockets.Num(), SkelSockets.Num(), Total);
        UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), UniqueSkelMeshSockets.Num(), UniqueSkelSockets.Num(), UniqueTotal);
        UE_LOG(LogTemp, Warning, TEXT("Found Duplicates: %s"), *((Total != UniqueTotal) ?FString("True") :FString("False")));
    }
    return BaseMesh;
}

エディタ内でコードをコンパイルすると、 スケルタル メッシュ コンポーネント と、 Skeletal Mesh Parameters タイプの公開された変数を使用して ActorBlueprint を作成できます。この変数では、マージするスケルタル メッシュだけでなく、これらのメッシュのマージ方法と追加のオプションを定義するいくつかのプロパティが提供されます。

SkelMeshParams.png

スケルタル メッシュのマージ方法の定義に使用できるオプションは次の通りです。

プロパティ

説明

Mesh Section Mappings

ソース メッシュのセクションを、マージされたセクション エントリにマップするためのオプションの配列です。

UVTransforms Per Mesh

各メッシュ内の UV の変換に使用するオプションの配列です。

Meshes to Merge

マージされるスケルタル メッシュです。

Strip Top LODs

入力メッシュから削除するトップ LOD の数です。

Needs Cpu Access

スポーン パーティクル エフェクトなどの理由で、マージ後のメッシュへの CPU によるアクセスが必要かどうかを設定します。

Skeleton Before

マージ前または後にスケルトンを更新するかどうかを設定します (スケルトンを提供する必要があります)。

Skeleton

マージしたメッシュで使用されるスケルトンです。生成されたスケルトンを使用する場合は空のままにします。

イベント グラフ 内で、 Event Begin Play で下のノード ネットワークを使用します。

クリックしてフルサイズで表示

しく作成したブループリント関数 Merge Meshes を使用して、Skeletal Mesh オブジェクト参照を返して Mesh Merge Parameters に渡すことができます。ブループリントに追加された スケルタル メッシュ コンポーネント は、使用する新しいスケルタル メッシュの設定においてターゲットとして使用することができ、Merge Meshes 関数呼び出しの戻り値にポイントできます。上記の例では、個別のメッシュがすべてマージされたときに再生するよう、スケルタル メッシュに対して Idleanimation を割り当てています。

Mesh Merge ブループリントをレベルに追加すると、 [Details (詳細)] パネル内で、使用する Meshes to Use Skeleton アセットなどの Mesh Merge Parameters を定義できます。

AssignedDetails-1.png

ランタイム時には、定義されたメッシュに基づいて、Mesh Merge 関数が実行されてスケルタル メッシュが組み立てられます。

比較表

Master Pose Component Copy Pose from Mesh Skeletal Mesh Merge のいずれの方法にも長所と短所があります。以下の表では、それぞれの方法に関連する設定とパフォーマンスにかかる負荷、さらに追加機能のサポート状況を比較します。

マスター

Copy Pose

Mesh Merge

Setup Cost

Min

Medium

High

Game Thread Cost

Min

High

Medium

Render Thread Cost

High

High

Low

Physics

No

AnimDynamics or RigidBody

Yes

Morph Target

Yes

Yes

No

Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
閉じる