본문 바로가기

Programming/Unreal

InstancedStaticMeshComponent Atlas Texture 사용하도록 Engine 코드변경하기

언리얼 엔진의 경우 InstacedStaticMeshComponent를 통해 한번에 여러 메시를 그릴 수 있는데 튜토리얼들을 보면 다 같은 텍스쳐를 


사용하는 경우만 있다. 


마인크래프트 같은 샌드박스 게임을 만들 경우 모래, 흙, 유리, 물, 용암 등의 여러 텍스쳐가 필요한데 이를 Atlas로 묶어서 할 수 있도록


언리얼 엔진을 바꿔보려한다.


우선 엔진 소스코드의 InstancedStaticMeshComponent.h 파일을 열어 코드를 수정해준다.


InstancedStaticMeshComponent.h


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Instanced Static Mesh Component의 Instace 정보를 가지는 구조체
USTRUCT()
struct FInstancedStaticMeshInstanceData
{
    GENERATED_USTRUCT_BODY()
 
    UPROPERTY(EditAnywhere, Category=Instances)
    FMatrix Transform;
 
    // SpriteIndex 를 저장할 변수 추가
    UPROPERTY(EditAnywhere, Category = Instances)
    float SpriteIndex;
    
    // Padding Byte용  Dummy들
    /* Unreal Engine에 Serialize를 하는 구조체의 경우 
       구조체 크기와 구조체의 각변수의 크기를 비교하는데
       서로 크기가 다를 경우 Memcpy에서 에러를 내뿜기 때문에
       Padding Byte Dummy값을 넣어 맞춰줘야 함.
    */
    // DummyStart
    UPROPERTY()
    float PaddingByteDummy1;
 
    UPROPERTY()
    float PaddingByteDummy2;
 
    UPROPERTY()
    float PaddingByteDummy3;
    // DummyEnd
 
    /** Legacy, this is now stored in FMeshMapBuildData.  Still serialized for backwards compatibility. */
    UPROPERTY()
    FVector2D LightmapUVBias_DEPRECATED;
 
    /** Legacy, this is now stored in FMeshMapBuildData.  Still serialized for backwards compatibility. */
    UPROPERTY()
    FVector2D ShadowmapUVBias_DEPRECATED;
 
    FInstancedStaticMeshInstanceData()
        : Transform(FMatrix::Identity)
        // 추가한 값 생성자에 추가
        , SpriteIndex(float(0.0f))
        , PaddingByteDummy1(float(0.0f))
        , PaddingByteDummy2(float(0.0f))
        , PaddingByteDummy3(float(0.0f))
        //
        , LightmapUVBias_DEPRECATED(ForceInit)
        , ShadowmapUVBias_DEPRECATED(ForceInit)
    {
    }
 
    friend FArchive& operator<<(FArchive& Ar, FInstancedStaticMeshInstanceData& InstanceData)
    {
        // @warning BulkSerialize: FInstancedStaticMeshInstanceData is serialized as memory dump
        // See TArray::BulkSerialize for detailed description of implied limitations.
        Ar << InstanceData.Transform << InstanceData.LightmapUVBias_DEPRECATED << InstanceData.ShadowmapUVBias_DEPRECATED;
        // Serialize 할 경우 들어오는 부분 각 변수값으 크기를 계산해줘야 하기 때문에 추가해줌
        Ar << InstanceData.SpriteIndex;
        Ar << InstanceData.PaddingByteDummy1;
        Ar << InstanceData.PaddingByteDummy2;
        Ar << InstanceData.PaddingByteDummy3;
        //
        return Ar;
    }
};
cs



아래로 조금 더 내려와 함수들을 추가 및 수정해준다.


1
2
3
4
5
6
// 이 함수의 경우 다른 Class에서 참조하고 있으므로 변경하지 않고 새로운 함수와 연결시켜주는게 좋다.
UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
    virtual int32 AddInstance(const FTransform& InstanceTransform);
// 새롭게 SpriteIndex를 받을 함수 추가
UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
    virtual int32 AddInstance_SpriteIndex(const FTransform& InstanceTransform, float SpriteIndex);
cs


아래 함수를 추가해준다.


1
2
3
4
5
6
7
8
9
10
11
// 각 인스턴스의 프라이트 값 변경 추가
    UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
    void SetInstanceSpriteIndex(int32 InstancedIndex, float SpriteIndex, bool Refresh);
    
// 바뀐 값을 다시 그리게할 리프레쉬 함수 추가
    UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
    void AllInstanceRefresh();
 
// 각 인스턴스의 스프라이트 값 가져오기 추가
    UFUNCTION(BlueprintCallable, Category = "Components|InstancedStaticMesh")
    int32 GetInstanceSpriteIndex(int32 InstancedIndex);
cs

아래로 더 내려와 SetupNewInstanceData 함수에 SpriteIndex를 받을 수 있도록 인자를 추가해준다.

1
void SetupNewInstanceData(FInstancedStaticMeshInstanceData& InOutNewInstanceData, int32 InInstanceIndex, const FTransform& InInstanceTransform, float SpriteIndex);
cs


이제 헤더파일의 수정과 추가는 끝났고 cpp파일을 수정과 추가를 해주도록 한다.


InstancedStaticMesh.cpp


FStaticMeshInstanceBuffer::Init 함수 수정


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
void FStaticMeshInstanceBuffer::Init(UInstancedStaticMeshComponent* InComponent, const TArray<TRefCountPtr<HHitProxy> >& InHitProxies)
{
    QUICK_SCOPE_CYCLE_COUNTER(STAT_FStaticMeshInstanceBuffer_Init);
    double StartTime = FPlatformTime::Seconds();
 
    bool bUseRemapTable = InComponent->PerInstanceSMData.Num() == InComponent->InstanceReorderTable.Num();
 
    int32 NumRealInstances = InComponent->PerInstanceSMData.Num();
    int32 NumRenderInstances = InComponent->GetNumRenderInstances();
 
    // Allocate the vertex data storage type.
    AllocateData();
 
    SetupCPUAccess(InComponent);
 
    NumInstances = NumRenderInstances;
    InstanceData->AllocateInstances(NumInstances);
 
    // Setup our random number generator such that random values are generated consistently for any
    // given instance index between reattaches
    
    // 스프라이트 인덱스를 추가했으므로 랜덤으로 값을 넘길필요가 없기에 주석처리
    //FRandomStream RandomStream( InComponent->InstancingRandomSeed );
 
    const FMeshMapBuildData* MeshMapBuildData = NULL;
    
    if (InComponent->LODData.Num() > 0)
    {
        MeshMapBuildData = InComponent->GetMeshMapBuildData(InComponent->LODData[0]);
    }
    
    for (int32 InstanceIndex = 0; InstanceIndex < NumRealInstances; InstanceIndex++)
    {
        const FInstancedStaticMeshInstanceData& Instance = InComponent->PerInstanceSMData[InstanceIndex];
        const int32 DestInstanceIndex = bUseRemapTable ? InComponent->InstanceReorderTable[InstanceIndex] : InstanceIndex;
        if (DestInstanceIndex != INDEX_NONE)
        {
            FVector2D LightmapUVBias = Instance.LightmapUVBias_DEPRECATED;
            FVector2D ShadowmapUVBias = Instance.ShadowmapUVBias_DEPRECATED;
 
            if (MeshMapBuildData && MeshMapBuildData->PerInstanceLightmapData.IsValidIndex(InstanceIndex))
            {
                LightmapUVBias = MeshMapBuildData->PerInstanceLightmapData[InstanceIndex].LightmapUVBias;
                ShadowmapUVBias = MeshMapBuildData->PerInstanceLightmapData[InstanceIndex].ShadowmapUVBias;
            }
 
            // RandomStream.GetFraction() 값을 Instance.SpriteIndex 로 대체
            InstanceData->SetInstance(DestInstanceIndex, Instance.Transform, Instance.SpriteIndex, LightmapUVBias, ShadowmapUVBias);
        }
    }
 
    SetPerInstanceEditorData(InComponent, InHitProxies);
 
    // Hide any removed instances
    int32 NumRemoved = InComponent->RemovedInstances.Num();
    if (NumRemoved)
    {
        check(bUseRemapTable);
        for (int32 InstanceIndex = 0; InstanceIndex < NumRemoved; InstanceIndex++)
        {
            const int32 DestInstanceIndex = InComponent->RemovedInstances[InstanceIndex];
            InstanceData->NullifyInstance(DestInstanceIndex);
        }
    }
 
    float ThisTime = (StartTime - FPlatformTime::Seconds()) * 1000.0f;
    if (ThisTime > 30.0f)
    {
        UE_LOG(LogStaticMesh, Display, TEXT("Took %6.2fms to set up instance buffer for %d instances for component %s."), ThisTime, NumRealInstances, *InComponent->GetFullName());
    }
}
cs


UInstancedStaticMeshComponent::AddInstance 수정


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int32 UInstancedStaticMeshComponent::AddInstance(const FTransform& InstanceTransform)
{
    // 다른 Class에서 참조하고있는 AddInstance를 그대로 보존하기 위해 수정
    return AddInstance_SpriteIndex(InstanceTransform, 0);
}
 
// Sprite 값을 받는 AddInstance 
int32 UInstancedStaticMeshComponent::AddInstance_SpriteIndex(const FTransform& InstanceTransform, float SpriteIndex)
{
    int InstanceIdx = PerInstanceSMData.Num();
 
    FInstancedStaticMeshInstanceData* NewInstanceData = new(PerInstanceSMData) FInstancedStaticMeshInstanceData();
    // SpriteIndex 추가
    SetupNewInstanceData(*NewInstanceData, InstanceIdx, InstanceTransform, SpriteIndex);
 
#if WITH_EDITOR
    if (SelectedInstances.Num())
    {
        SelectedInstances.Add(false);
    }
#endif
 
    ReleasePerInstanceRenderData();
    MarkRenderStateDirty();
 
    PartialNavigationUpdate(InstanceIdx);
 
    return InstanceIdx;
}
cs


UInstancedStaticMeshComponent::SetupNewInstanceData 수정


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void UInstancedStaticMeshComponent::SetupNewInstanceData(FInstancedStaticMeshInstanceData& InOutNewInstanceData, int32 InInstanceIndex, const FTransform& InInstanceTransform, float SpriteIndex)
{
    InOutNewInstanceData.Transform = InInstanceTransform.ToMatrixWithScale();
    // SpriteIndex 추가
    InOutNewInstanceData.SpriteIndex = SpriteIndex;
    InOutNewInstanceData.LightmapUVBias_DEPRECATED = FVector2D( -1.0f, -1.0f );
    InOutNewInstanceData.ShadowmapUVBias_DEPRECATED = FVector2D( -1.0f, -1.0f );
    if (bPhysicsStateCreated)
    {
        if (InInstanceTransform.GetScale3D().IsNearlyZero())
        {
            InstanceBodies.Insert(nullptr, InInstanceIndex);
        }
        else
        {
            FBodyInstance* NewBodyInstance = new FBodyInstance();
            int32 BodyIndex = InstanceBodies.Insert(NewBodyInstance, InInstanceIndex);
            check(InInstanceIndex == BodyIndex);
            InitInstanceBody(BodyIndex, NewBodyInstance);
        }
    }
}
cs


추가로 만든 함수 내용 추가.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 스프라이트 값 변경을 위한 함수
void UInstancedStaticMeshComponent::SetInstanceSpriteIndex(int32 InstancedIndex, float SpriteIndex, bool Refresh)
{
    if (PerInstanceSMData.Num() > InstancedIndex)
    {
        FInstancedStaticMeshInstanceData* InstanceData = &PerInstanceSMData[InstancedIndex];
        InstanceData->SpriteIndex = SpriteIndex;
        FTransform transform;
        transform.SetFromMatrix(InstanceData->Transform);
        UpdateInstanceTransform(InstancedIndex, transform, false, Refresh, false);
    }
}
 
// UpdateTransform에서 모든 위치 혹은 SetInstanceSpriteIndex에서 스프라이트를 변경 후 한번에 다시 그리도록 호출해주는게 좋다.
void UInstancedStaticMeshComponent::AllInstanceRefresh()
{
    MarkRenderStateDirty();
}
 
// 해당 인덱스의 스프라이트 값을 알고 싶을 때 사용
int32 UInstancedStaticMeshComponent::GetInstanceSpriteIndex(int32 InstancedIndex)
{
    if (PerInstanceSMData.Num() > InstancedIndex)
    {
        return PerInstanceSMData[InstancedIndex].SpriteIndex;
    }
    return -1;
}
cs



// 추가하지 않아도 되는 내용

// 에디터에서 값변경 시 다시 그리도록 하는 코드

// cpp에서 UInstancedStaticMeshComponent::PostEditChangeChainProperty 함수를 찾아 변경해주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void UInstancedStaticMeshComponent::PostEditChangeChainProperty(FPropertyChangedChainEvent& PropertyChangedEvent)
{
    if(PropertyChangedEvent.Property != NULL && PropertyChangedEvent.Property->GetFName() == "PerInstanceSMData")
    {
        if(PropertyChangedEvent.ChangeType == EPropertyChangeType::ArrayAdd)
        {
            int32 AddedAtIndex = PropertyChangedEvent.GetArrayIndex(PropertyChangedEvent.Property->GetFName().ToString());
            check(AddedAtIndex != INDEX_NONE);
            SetupNewInstanceData(PerInstanceSMData[AddedAtIndex], AddedAtIndex, FTransform::Identity, 0.0f);
 
            // added via the property editor, so we will want to interactively work with instances
            bHasPerInstanceHitProxies = true;
        }
 
        ReleasePerInstanceRenderData();
        MarkRenderStateDirty();
    }
    else if (PropertyChangedEvent.Property != NULL && PropertyChangedEvent.Property->GetFName() == "Transform")
    {
        ReleasePerInstanceRenderData();
        MarkRenderStateDirty();
    }
    // 에디터에서 Sprite index가 변경될 경우에도 데이터를 다시 적용해서 그리도록 추가해준다.
    else if (PropertyChangedEvent.Property != NULL && PropertyChangedEvent.Property->GetFName() == "SpriteIndex")
    {
        ReleasePerInstanceRenderData();
        MarkRenderStateDirty();
    }
 
    Super::PostEditChangeChainProperty(PropertyChangedEvent);
}
 
cs

// 


해당 값을 적용해 사용할 Material


SpriteIndex가 들어오는 PerInstanceRandom에 0.5를 더해준 이유는 2가 넘어올 경우 1.99999 같은게 올 수 있기 때문에 문제가 된다.


그래서 0.5를 더한 후 버림을 해주면 제대로 된 값을 받을 수 있다.