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 |
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를 더한 후 버림을 해주면 제대로 된 값을 받을 수 있다.