Unreal Engine 5
August 19

Лента Мёбиуса. Часть 1.

В свой  pet-проект я захотел добавить процедурный уровень на основе ленты Мёбиуса. Пока я собирал функционал и исследовал возможности движка, я узнал много полезных вещей, в частности связанных с кастомной гравитацией и с динамическими мешами в UE. Решил поделиться с вами этими находками.

Уровень лента

.Для начала я определился с требованиями к генератору:

  1. Размер и положение ленты должно быть удобно для настройки.
  2. Лента должна быть процедурно генерируемая.
  3. Алгоритм должен быть простой и достаточно быстрый, чтобы я мог использовать её в рантайме со случайными параметрами, чтобы генерировать новые уровни.

С такими требованиями я принял пару решений. За основу взял зацикленный сплайн в USplineComponent.  Для отображения сначала попробовал использовать UProceduralMeshComponent, но потом поменял свое решение в пользу UDynamicMeshComponent. Во-первых, потому что динамические меши добавили в 5.0 и их сейчас активно развивают. Во вторых, для них написано много реализации в UGeometryScriptLibrary_MeshPrimitiveFunctions, куда я могу обращаться за примерами.

Генерация Ориентации.

Способом генерации я выбрал вращение вертикального вектора, в частности:

  1. Разбиваем весь сплайн на равные участки нужной длины.(это позволит мне контролировать детализацию ленты)
  2. Проходим все участки в цикле, каждый раз поворачивая вертикальный вектор на угол равный 360.0/количество участков.
  3. Генерируем вершины нашей ленты, и соединяем их в треугольники, которые отправляем в dynamicmesh.

Для проверки теории я решил быстро реализовать первые два пункта и проверить, что я правильно разобрался в интерфейсе USplineComponent.

UCLASS()
class FPSGAME_API ACRMobiusStrip : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor's properties
	ACRMobiusStrip();

	UFUNCTION(CallInEditor, BlueprintCallable)
	void  GenerateStrip();

protected:
	UPROPERTY(EditAnywhere)
	float SegmentLenght = 100;
	UPROPERTY(EditAnywhere)
	float SegmentWidth = 500;

	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class UDynamicMeshComponent* MobiusMesh;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class USplineComponent* SplineComponent;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class USplineComponent* MobiusSplineComponent;
};

N.B. полезный атрибут функции в UE CallInEditor. Он добавляет кнопку в редактор, которая и вызывает функцию. Это очень удобно для тестирования генераторов и неигровых логик.

void ACRMobiusStrip::GenerateStrip()
{
   float splineLenght = SplineComponent->GetSplineLength();
   int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
   FVector UpVector = FVector::UpVector;
  
   for (int index = 0; index <= numOfSteps; ++index )
   {
      FVector Location = SplineComponent->GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::World);
      FRotator Direction = SplineComponent->GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
      FVector DirectionUp = UpVector.RotateAngleAxis(360.0/numOfSteps *index, Direction.Vector());


      DrawDebugDirectionalArrow(GetWorld(), Location, Location + DirectionUp *200.f, 20, FColor::Green, false);
   }
}

Я убрал из статьи инициализации компонентов и конструктор, ибо они совершенно обычные и я ничего интересного там не делал.

В результате получаем следующую картину:

Треугольники

Теперь осталось построить прямоугольники на каждом участке, и лента готова.

Подглядев на то, как с UDynamicMeshComponent работают в движке, я увидел два способа: собрать свою версию FMeshShapeGenerator , либо взять меш из компонента и настраивать вершины и треугольники напрямую. Я выбрал второй путь и остался доволен, так как это позволило мне контролировать материалы и UV в одном месте.

Давайте начнем создавать вершины. На каждом участке делаем две вершины в плоскости, для которой наш повернутый вектор будет нормалью, сделав отступ вправо и влево от точки на сплайне.

FVector Vert0 = Location + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
FVector Vert1 = Location + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;

Добавим эти вершины в наш меш, сохраним индекс первой вершины:

int StartVertIndex = MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert0));
MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert1));

Теперь для всех шагов, кроме первого (на первом у нас всего 2 вершины, треугольник не сделать), возьмем вершины с прошлого шага и сделаем из них 2 треугольника.

if (index != 0)
{
   UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
   MobiusMesh3->AppendTriangle(Tri);       
   Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);       
   MobiusMesh3->AppendTriangle(Tri);
}


PrevStartVertIndex = StartVertIndex;

Получаем следующий меш:

N.B. Есть несколько нюансов работы с UDynamicMeshComponent , которые я узнал, работая с этой лентой, и о которых хотел бы рассказать.

Чтобы получить меш внутри компонента и работать, есть удобная функция 

DynamicMesh3* MobiusMesh3 = MobiusMesh->GetDynamicMesh()->GetMeshPtr(); 

Не забудьте включить "DynamicMesh" в публичные зависимости вашего проекта, иначе линтер не даст вам собрать проект.

Чтобы применить изменения в меше, нужно вызвать NotifyMeshUpdated на компоненте.

Сразу видны три проблемы: первая - это не лента мебиуса, а просто перекрученная лента. И вправду - мой вектор нормали делает полный оборот, и поэтому получается две поверхности. Заменим нашу константу на настраиваемый параметр количества полуоборотов, что даст мне возможность генерировать любые ленты. Вторая проблема - это отсутствие последних треугольников, которые замкнут петлю. С этой проблемой мы разберемся в конце. Последняя проблема - это то, что лента видна только с одной стороны. Эту проблему можно решить с помощью шейдера, но я все равно хотел добавить толщины ленте (так как игрок будет двигаться по ней, он должен иметь возможность увидеть грань).

Прежде чем добавлять толщину, приведу полный код с исправленной проблемой полного оборота.

UCLASS()
class FPSGAME_API ACRMobiusStrip : public AActor
{
	GENERATED_BODY()
	.
	.
	.
	UPROPERTY(EditAnywhere)
	int NumberOfHalfTurns = 1;

	float TotalAngle = 180.0f;
	.
	.
	.
}
void ACRMobiusStrip::GenerateStrip()
{
	float splineLenght = SplineComponent->GetSplineLength();
	int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
	FVector UpVector = FVector::UpVector;

	TotalAngle = NumberOfHalfTurns * 180.0f;
	
	FDynamicMesh3* MobiusMesh3 = MobiusMesh->GetDynamicMesh()->GetMeshPtr();
	MobiusMesh3->Clear();

	int PrevStartVertIndex = 0;

	for (int index = 0; index <= numOfSteps; ++index )
	{
		FVector Location = SplineComponent->GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FRotator Direction = SplineComponent->GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FVector DirectionUp = UpVector.RotateAngleAxis(360.0/numOfSteps *index, Direction.Vector());
		FRotator Rotator = FRotationMatrix::MakeFromZX(DirectionUp, Direction.Vector()).Rotator();

		FVector Vert0 = Location +  Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		FVector Vert1 = Location - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		
		int StartVertIndex = MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert0));
		MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert1));
		
		if (index != 0)
		{
			UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
			MobiusMesh3->AppendTriangle(Tri);			
			Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
			MobiusMesh3->AppendTriangle(Tri);
		}

		PrevStartVertIndex = StartVertIndex;
	}

Толщина

Теперь добавим толщину: нам понадобятся еще две вершины. Наши старые вершины поднимем на половину заданной высоты (SegmentDepth) вдоль нормали нашей плоскости, а новые опустим вниз.

FVector Vert2 = Location - DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
FVector Vert3 = Location - DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
.
.
.
MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert2));
MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert3));

Теперь у нас 4 узла и мы можем собрать верхнюю, нижнюю и боковые плоскости нашей “ленты”.

//up plane
UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
MobiusMesh3->AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
MobiusMesh3->AppendTriangle(Tri);
			
//bottom plane
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 3, StartVertIndex + 3, StartVertIndex + 2 );
MobiusMesh3->AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex + 2, PrevStartVertIndex + 2, PrevStartVertIndex + 3 );
MobiusMesh3->AppendTriangle(Tri);

//left  side
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex+ 2 , StartVertIndex );
MobiusMesh3->AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex, PrevStartVertIndex+ 2 );
MobiusMesh3->AppendTriangle(Tri);			

//right side
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex +1, StartVertIndex + 3 );
MobiusMesh3->AppendTriangle(Tri);			
Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex + 3, PrevStartVertIndex + 1);
MobiusMesh3->AppendTriangle(Tri);

Запускаем и получаем результат

Теперь давайте замкнем нашу петлю. С помощью блокнота и ручки я сумел понять, какие вершины с какими образовывают треугольник, и написал отдельную логику для последней секции.

//we did half full  turn we need consider it in indexes
if (index == numOfSteps && NumberOfHalfTurns % 2 != 0)
{
	//up plane
	UE::Geometry::FIndex3i Tri(PrevStartVertIndex + 3, StartVertIndex, StartVertIndex + 1 );
	MobiusMesh3->AppendTriangle(Tri);
	Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 2,PrevStartVertIndex + 3);			
	MobiusMesh3->AppendTriangle(Tri);				
				
	//bottom plane
	Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, StartVertIndex + 2,  PrevStartVertIndex + 1);
	MobiusMesh3->AppendTriangle(Tri);
	Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1 , PrevStartVertIndex , StartVertIndex + 3);
	MobiusMesh3->AppendTriangle(Tri);			
				
	//left  side			
	Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex+ 2 , StartVertIndex );
	MobiusMesh3->AppendTriangle(Tri);	
	Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex+3, PrevStartVertIndex+ 1 );
	MobiusMesh3->AppendTriangle(Tri);	
}

Я не стал подробно объяснять, как я собирал треугольники из вершин. Там был простой и прямолинейный подход. Но если это кажется полезным, оставьте комментарий, и я подробно распишу как я собирал треугольники.

Теперь наша петля замкнута:

Первое, что я сделал - попробовал запрыгнуть на неё персонажем. Естественно коллизий у нее не было. Теоретически можно было завести кучу коробочек для моей ленты, но с учетом того, что она одна на уровне, и меш достаточно простой, я доверился оптимизации UE и просто включил сложные коллизии:

MobiusMesh->SetComplexAsSimpleCollisionEnabled(true);

UV и материалы.

Для того, чтобы включить UV развертку и материалы на меше, на нем надо активировать атрибуты с помощью функции EnableAttributes().

MobiusMesh3->EnableAttributes();

Для того, чтобы добавлять UV к конкретным треугольникам, удобно взять ссылки на два объекта FDynamicMeshUVOverlay и  FDynamicMeshMaterialAttribute.

UE::Geometry::FDynamicMeshUVOverlay* UVOverlay 
                           = MobiusMesh3->Attributes()->PrimaryUV();
UE::Geometry::FDynamicMeshMaterialAttribute* MaterialAttribute
                           = MobiusMesh3->Attributes()->GetMaterialID();

Для того, чтобы проставить UV или материал, надо знать индекс треугольника (его возвращает функция SetTriangle)

UVOverlay->SetTriangle(tid, UVUpTri);
MaterialAttribute->SetNewValue(tid, 1);

Для того, чтобы назначить конкретный материал на меш, нужно его передать уже в компонент.

MobiusMesh->SetMaterial(0, BaseMaterial);

Я завел поле для материала, в моем классе.

UPROPERTY(EditAnywhere)
UMaterialInterface* BaseMaterial;

Для UV развертки я решил, что у меня будет очень длинная лента в UV пространстве, которую я переадресую на мои треугольники равномерно. Будем добавлять точки к нашему UVOverlay  на каждом шаге цикла, генерируя небольшую полоску в UV пространстве, на которую будем проецировать все наши треугольники в этом шаге цикла.

Добавляем две вершины.

float UVYOffest = SegmentLenght * index/ SegmentWidth;
int StartUVIndex = UVOverlay->AppendElement(FVector2f(0.0f, UVYOffest));
UVOverlay->AppendElement(FVector2f(1.0f,UVYOffest));

Создаем два треугольника в UV пространстве.

UE::Geometry::FIndex3i UVUpTri(PrevStartUVIndex, StartUVIndex, StartUVIndex + 1 );
UE::Geometry::FIndex3i UVDownTri(StartUVIndex+1, PrevStartUVIndex+1, PrevStartUVIndex);

Сохраняем индексы наших треугольников и добавляем адресацию между треугольниками меша и UVOverlay

UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
int tid = MobiusMesh3->AppendTriangle(Tri);      
UVOverlay->SetTriangle(tid, UVUpTri);


Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);        
tid = MobiusMesh3->AppendTriangle(Tri);
UVOverlay->SetTriangle(tid, UVDownTri);

Повторяем для всех треугольников. Назначаем тестовый материал для дебага UV.

N.B. для последнего шага цикла, когда мы замыкаем петлю, мне понадобилась дополнительная магия с UV, чтобы все красиво сошлось в месте начала петли. Я честно добился правильной развертки методом проб и ошибок, но наверное это можно было вычислить как-то заранее.

В итоге я получил вот такой меш.

P.S.

Финальный код если кто то решит его прочитать

void ACRMobiusStrip::GenerateStrip()
{
	float splineLenght = SplineComponent->GetSplineLength();
	int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
	FVector UpVector = FVector::UpVector;

	TotalAngle = NumberOfHalfTurns * 180.0f;
	
	FDynamicMesh3* MobiusMesh3 = MobiusMesh->GetDynamicMesh()->GetMeshPtr();
	MobiusMesh3->Clear();
	MobiusMesh3->EnableAttributes();
	MobiusMesh3->Attributes()->EnableMaterialID();
	UE::Geometry::FDynamicMeshUVOverlay* UVOverlay = MobiusMesh3->Attributes()->PrimaryUV();
	UE::Geometry::FDynamicMeshMaterialAttribute* MaterialAttribute = MobiusMesh3->Attributes()->GetMaterialID();
	
	int PrevStartVertIndex = 0;
	int PrevStartUVIndex = 0;
	

	
	for (int index = 0; index <= numOfSteps; ++index )
	{
		FVector Location = SplineComponent->GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FRotator Direction = SplineComponent->GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FVector DirectionUp = UpVector.RotateAngleAxis(TotalAngle/numOfSteps *index, Direction.Vector());
		FRotator Rotator = FRotationMatrix::MakeFromZX(DirectionUp, Direction.Vector()).Rotator();
		int StartVertIndex = 0;
		if (index != numOfSteps)
		{
			FVector Vert0 = Location + DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert1 = Location + DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert2 = Location - DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert3 = Location - DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		
			StartVertIndex = MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert0));
			MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert1));
			MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert2));
			MobiusMesh3->AppendVertex(UE::Geometry::FVertexInfo(Vert3));
		}

		float UVYOffest = SegmentLenght * index/ SegmentWidth;
		int StartUVIndex = UVOverlay->AppendElement(FVector2f(0.0f, UVYOffest));
		UVOverlay->AppendElement(FVector2f(1.0f,UVYOffest));
		
		if (index != 0)
		{
			UE::Geometry::FIndex3i UVUpTri(PrevStartUVIndex, StartUVIndex, StartUVIndex + 1 );
			UE::Geometry::FIndex3i UVDownTri(StartUVIndex+1, PrevStartUVIndex+1, PrevStartUVIndex);
			//we did half full  turn we need consider it in indexes
			if (index == numOfSteps && NumberOfHalfTurns % 2 != 0)
			{
				//up plane
				UE::Geometry::FIndex3i Tri(PrevStartVertIndex + 3, StartVertIndex, StartVertIndex + 1 );
				int tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);
		
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 2,PrevStartVertIndex + 3);			
				tid = MobiusMesh3->AppendTriangle(Tri);
				UVOverlay->SetTriangle(tid, UVDownTri);
				
				//bottom plane
				UVUpTri = UE::Geometry::FIndex3i  (StartUVIndex, StartUVIndex + 1, PrevStartUVIndex+1 );
				UVDownTri =  UE::Geometry::FIndex3i(PrevStartUVIndex+1, PrevStartUVIndex, StartUVIndex);
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, StartVertIndex + 2,  PrevStartVertIndex + 1);
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);
			
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1 , PrevStartVertIndex , StartVertIndex + 3);
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);
				
				//left  side
				UVUpTri = UE::Geometry::FIndex3i  (PrevStartUVIndex , StartUVIndex, StartUVIndex + 1);
				UVDownTri =  UE::Geometry::FIndex3i(StartUVIndex + 1, PrevStartUVIndex+1, PrevStartUVIndex);

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex+ 2 , StartVertIndex);
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex+3, PrevStartVertIndex+ 1 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);

				//right side
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex +1, StartVertIndex + 3 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex , PrevStartVertIndex + 2);
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);		
				
			}
			else
			{
				//up plane
				UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
				int tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);
		
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
				tid = MobiusMesh3->AppendTriangle(Tri);
				UVOverlay->SetTriangle(tid, UVDownTri);
				//bottom plane
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 3, StartVertIndex + 3, StartVertIndex + 2 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);
			
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 2, PrevStartVertIndex + 2, PrevStartVertIndex + 3 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);
				//left  side

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex+ 2 , StartVertIndex );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex, PrevStartVertIndex+ 2 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);

				//right side

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex +1, StartVertIndex + 3 );
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex + 3, PrevStartVertIndex + 1);
				tid = MobiusMesh3->AppendTriangle(Tri);			
				UVOverlay->SetTriangle(tid, UVDownTri);
			}		
		}
		PrevStartUVIndex = StartUVIndex;
		PrevStartVertIndex = StartVertIndex;
	}

	MobiusMesh->NotifyMeshUpdated();
	MobiusMesh->SetMaterial(0, BaseMaterial);
}