Berpikir dengan Portal: membuat portal di Unreal Engine 4

gambar

Pada artikel ini saya akan memberi tahu Anda cara membuat portal di Unreal Engine 4. Saya tidak menemukan sumber yang menjelaskan sistem seperti itu secara terperinci (memantau melalui portal dan melewati mereka), jadi saya memutuskan untuk menulis sendiri.

Apa itu portal?


Mari kita mulai dengan contoh dan penjelasan tentang apa itu portal. Cara termudah untuk menggambarkan portal sebagai cara perjalanan dari satu ruang ke ruang lainnya. Dalam beberapa permainan populer, konsep ini digunakan untuk efek visual dan bahkan untuk mekanisme permainan:

Contoh Portal Game (GIF)


Antichamber (2013) dan Portal (2007)


Prey, 2006

Dari tiga game, yang paling terkenal mungkin Portal, tapi saya pribadi selalu mengagumi Prey dan dialah yang ingin saya salin. Pernah saya mencoba mengimplementasikan versi saya sendiri di Unreal Engine 4, tetapi saya tidak benar-benar berhasil, karena mesinnya tidak memiliki fungsionalitas. Meskipun demikian, saya berhasil melakukan eksperimen ini:


Namun, hanya dalam versi baru dari Unreal Engine akhirnya saya berhasil mencapai efek yang diinginkan:


Portal - bagaimana cara kerjanya?


Sebelum melanjutkan dengan spesifik, mari kita lihat gambaran umum tentang cara kerja portal.

Sebenarnya, portal adalah jendela yang tidak keluar, tetapi ke tempat lain, yaitu, kami secara lokal menetapkan sudut pandang spesifik relatif terhadap objek dan mereplikasi sudut pandang ini di tempat lain. Dengan menggunakan prinsip ini, kita dapat menghubungkan dua ruang, meskipun jaraknya sangat jauh. Jendela menyerupai topeng yang memungkinkan kita untuk mengetahui di mana dan kapan menampilkan ruang lain, bukan yang asli. Karena titik awal pandangan direplikasi di tempat lain, ini memberi kita ilusi kesinambungan.


Dalam gambar ini, perangkat tangkap (SceneCapture di UE4) terletak di depan ruang yang sesuai dengan ruang yang dilihat dari sudut pandang pemain. Segala sesuatu yang terlihat setelah garis digantikan oleh apa yang dapat dilihat oleh tangkapan. Karena perangkat penangkap dapat ditempatkan di antara pintu dan benda-benda lain, penting untuk menggunakan apa yang disebut "pesawat kliping". Dalam kasus portal, kami ingin pesawat kliping dekat untuk menutupi objek yang terlihat di depan portal.

Untuk meringkas. Kami membutuhkan:

  • Lokasi Pemain
  • Titik Masuk Portal
  • Titik Keluar Portal
  • Perangkat kliping dengan pesawat kliping

Bagaimana cara menerapkan ini di Unreal Engine?

Saya membangun sistem saya berdasarkan dua kelas utama yang dikelola oleh PlayerController dan Character . Kelas Portal adalah titik masuk portal sejati, yang titik pandang / keluarnya adalah aktor Target. Ada juga Manajer Portal , yang dihasilkan oleh PlayerController dan diperbarui oleh Karakter untuk mengelola setiap portal di tingkat dan memperbaruinya, serta untuk memanipulasi objek SceneCapture (yang umum untuk semua portal).

Ingatlah bahwa tutorial ini mengharapkan Anda memiliki akses ke kelas Character dan PlayerController dari kode. Dalam kasus saya, mereka disebut ExedreCharacter dan ExedrePlayerController.

Membuat Kelas Aktor Portal


Mari kita mulai dengan aktor portal, yang akan digunakan untuk mengatur "windows" di mana kita akan melihat levelnya. Tugas aktor adalah memberikan informasi tentang pemain untuk menghitung berbagai posisi dan belokan. Dia juga akan terlibat dalam mengenali apakah pemain melintasi portal, dan teleportasinya.

Sebelum memulai diskusi rinci tentang aktor, izinkan saya menjelaskan beberapa konsep yang saya buat untuk mengelola sistem portal:

  • Untuk penolakan perhitungan yang nyaman, portal memiliki status aktif-tidak aktif. Keadaan ini diperbarui oleh Portal Manager.
  • Portal memiliki sisi depan dan belakang yang ditentukan oleh posisi dan arahnya (vektor ke depan).
  • Untuk mengetahui apakah pemain melewati portal, ia menyimpan posisi pemain sebelumnya dan membandingkannya dengan yang sekarang. Jika pada ukuran sebelumnya pemain berada di depan portal, dan pada saat ini - di belakangnya, maka kami percaya bahwa pemain melewatinya. Perilaku sebaliknya diabaikan.
  • Portal memiliki volume terbatas, sehingga tidak melakukan perhitungan dan memeriksa sampai pemain ada di volume ini. Contoh: Abaikan persimpangan jika pemain tidak benar-benar menyentuh portal.
  • Lokasi pemain dihitung dari lokasi kamera untuk memastikan perilaku yang benar ketika sudut pandang melintasi portal tetapi bukan tubuh pemain.
  • Portal menerima Target Render, yang menampilkan sudut pandang yang berbeda dalam setiap ukuran jika tekstur waktu berikutnya salah dan perlu diganti.
  • Portal menyimpan tautan ke aktor lain yang disebut Target, untuk mengetahui di mana ruang lain yang harus dihubungi.

Menggunakan aturan-aturan ini, saya membuat kelas ExedrePortal baru yang diwarisi dari AActor sebagai titik awal. Inilah judulnya:

#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortal.generated.h" UCLASS() class EXEDRE_API AExedrePortal : public AActor { GENERATED_UCLASS_BODY() protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; //Status of the Portal (being visualized by the player or not) UFUNCTION(BlueprintPure, Category="Exedre|Portal") bool IsActive(); UFUNCTION(BlueprintCallable, Category="Exedre|Portal") void SetActive( bool NewActive ); //Render target to use to display the portal UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Exedre|Portal") void ClearRTT(); UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Exedre|Portal") void SetRTT( UTexture* RenderTexture ); UFUNCTION(BlueprintNativeEvent, Category="Exedre|Portal") void ForceTick(); //Target of where the portal is looking UFUNCTION(BlueprintPure, Category="Exedre|Portal") AActor* GetTarget(); UFUNCTION(BlueprintCallable, Category="Exedre|Portal") void SetTarget( AActor* NewTarget ); //Helpers UFUNCTION(BlueprintCallable, Category="Exedre|Portal") bool IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ); UFUNCTION(BlueprintCallable, Category="Exedre|Portal") bool IsPointCrossingPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ); UFUNCTION(BlueprintCallable, Category="Exedre|Portal") void TeleportActor( AActor* ActorToTeleport ); protected: UPROPERTY(BlueprintReadOnly) USceneComponent* PortalRootComponent; private: bool bIsActive; AActor* Target; //Used for Tracking movement of a point FVector LastPosition; bool LastInFront; }; 

Seperti yang Anda lihat, ada sebagian besar perilaku yang dijelaskan di sini. Sekarang mari kita lihat bagaimana mereka diproses di dalam tubuh (.cpp).



Perancang di sini sedang mempersiapkan komponen root. Saya memutuskan untuk membuat dua komponen root, karena aktor portal akan menggabungkan efek grafis dan tabrakan / pengenalan. Jadi saya membutuhkan cara sederhana untuk menentukan di mana jendela / portal pesawat, tanpa perlu fitur bluetooth atau trik lainnya. PortalRootComponent akan menjadi dasar untuk semua perhitungan yang terkait dengan portal.

Root portal diatur ke dinamis, seandainya kelas Blueprint menghidupkannya (misalnya, menggunakan animasi buka / tutup).

 // Sets default values AExedrePortal::AExedrePortal(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = true; bIsActive = false; RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent")); RootComponent->Mobility = EComponentMobility::Static; PortalRootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("PortalRootComponent")); PortalRootComponent->SetupAttachment( GetRootComponent() ); PortalRootComponent->SetRelativeLocation( FVector(0.0f, 0.0f, 0.0f) ); PortalRootComponent->SetRelativeRotation( FRotator(0.0f, 0.0f, 0.0f) ); PortalRootComponent->Mobility = EComponentMobility::Movable; } 



Hanya ada fungsi Get and Set, dan tidak lebih. Kami akan mengelola keadaan aktivitas dari tempat lain.

 bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; } 



Acara cetak biru, saya tidak melakukan apa pun di kelas C ++.

 void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { } 



Fungsi Dapatkan dan Tetapkan untuk aktor Target. Tidak ada yang lebih rumit di bagian ini juga.

 AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; } 



Dengan fungsi ini, kita dapat dengan mudah memeriksa apakah suatu titik ada di depan sebuah pesawat, dan dalam kasus kita itu adalah sebuah portal. Fungsi ini menggunakan struktur FPlane dari mesin UE4 untuk melakukan perhitungan.

 bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point ); //If < 0 means we are behind the Plane //See : http://api.unrealengine.com/INT/API/Runtime/Core/Math/FPlane/PlaneDot/index.html return ( PortalDot >= 0 ); } 



Fungsi ini memeriksa untuk melihat apakah titik telah melewati bidang portal. Di sinilah kita menggunakan posisi lama untuk mengetahui bagaimana perilaku titik tersebut. Fungsi ini umum sehingga dapat bekerja dengan aktor apa pun, tetapi dalam kasus saya hanya digunakan dengan pemain.

Fungsi menciptakan arah / segmen antara lokasi sebelumnya dan saat ini, dan kemudian memeriksa apakah mereka memotong bidang. Jika demikian, maka kami memeriksa apakah melintasi ke arah yang benar (depan ke belakang?).

 bool AExedrePortal::IsPointCrossingPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FVector IntersectionPoint; FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point ); bool IsCrossing = false; bool IsInFront = PortalDot >= 0; bool IsIntersect = FMath::SegmentPlaneIntersection( LastPosition, Point, PortalPlane, IntersectionPoint ); //Did we intersect the portal since last Location ? //If yes, check the direction : crossing forward means we were in front and now at the back //If we crossed backward, ignore it (similar to Prey 2006) if( IsIntersect && !IsInFront && LastInFront ) { IsCrossing = true; } //Store values for Next check LastInFront = IsInFront; LastPosition = Point; return IsCrossing; } 

Aktor Teleport


Bagian terakhir dari aktor portal yang akan kita lihat adalah fungsi TeleportActor () .

Saat memindahkan aktor dari titik A ke titik B, Anda perlu meniru gerakan dan posisinya. Misalnya, jika seorang pemain masuk ke portal, maka dalam kombinasi dengan efek visual yang cocok, akan tampak baginya bahwa ia melewati pintu biasa.

Persimpangan portal terasa seperti bergerak dalam garis lurus, tetapi dalam kenyataannya sesuatu yang sama sekali berbeda terjadi. Setelah keluar dari portal, pemain mungkin berada dalam konteks yang sangat berbeda. Pertimbangkan sebuah contoh dari Portal:


Seperti yang Anda lihat, ketika melintasi portal, kamera berputar relatif terhadap vektor maju (rotate). Ini karena titik awal dan akhir sejajar dengan bidang yang berbeda:


Oleh karena itu, agar ini berfungsi, kita perlu mengubah gerakan pemain menjadi ruang relatif portal untuk mengubahnya menjadi ruang Target. Dengan menerapkan ini, kita dapat yakin bahwa setelah memasuki portal dan keluar dari sisi lain, pemain akan disejajarkan dengan benar sehubungan dengan ruang. Ini berlaku tidak hanya pada posisi dan rotasi aktor, tetapi juga pada kecepatannya .

Jika kita teleport aktor tanpa perubahan, mengubahnya menjadi rotasi lokal, maka sebagai hasilnya, aktor dapat menemukan dirinya terbalik. Ini mungkin cocok untuk objek, tetapi tidak berlaku untuk karakter atau pemain itu sendiri. Anda perlu mengubah posisi aktor, seperti yang ditunjukkan di atas dalam contoh dari Portal.

 void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; } //------------------------------- //Retrieve and save Player Velocity //(from the Movement Component) //------------------------------- FVector SavedVelocity = FVector::ZeroVector; AExedreCharacter* EC = nullptr; if( ActorToTeleport->IsA( AExedreCharacter::StaticClass() ) ) { EC = Cast<AExedreCharacter>( ActorToTeleport ); SavedVelocity = EC->GetCharMovementComponent()->GetCurrentVelocity(); } //------------------------------- //Compute and apply new location //------------------------------- FHitResult HitResult; FVector NewLocation = UTool::ConvertLocationToActorSpace( ActorToTeleport->GetActorLocation(), this, Target ); ActorToTeleport->SetActorLocation( NewLocation, false, &HitResult, ETeleportType::TeleportPhysics ); //------------------------------- //Compute and apply new rotation //------------------------------- FRotator NewRotation = UTool::ConvertRotationToActorSpace( ActorToTeleport->GetActorRotation(), this, Target ); //Apply new rotation ActorToTeleport->SetActorRotation( NewRotation ); //------------------------------- //If we are teleporting a character we need to //update its controller as well and reapply its velocity //------------------------------- if( ActorToTeleport->IsA( AExedreCharacter::StaticClass() ) ) { //Update Controller AExedrePlayerController* EPC = EC->GetPlayerController(); if( EPC != nullptr ) { NewRotation = UTool::ConvertRotationToActorSpace( EPC->GetControlRotation(), this, Target ); EPC->SetControlRotation( NewRotation ); } //Reapply Velocity (Need to reorient direction into local space of Portal) { FVector Dots; Dots.X = FVector::DotProduct( SavedVelocity, GetActorForwardVector() ); Dots.Y = FVector::DotProduct( SavedVelocity, GetActorRightVector() ); Dots.Z = FVector::DotProduct( SavedVelocity, GetActorUpVector() ); FVector NewVelocity = Dots.X * Target->GetActorForwardVector() + Dots.Y * Target->GetActorRightVector() + Dots.Z * Target->GetActorUpVector(); EC->GetCharMovementComponent()->Velocity = NewVelocity; } } //Cleanup Teleport LastPosition = NewLocation; } 



Seperti yang mungkin Anda perhatikan, untuk memanggil rotasi / posisi, saya memanggil fungsi eksternal. Mereka dipanggil dari kelas pengguna UTool, yang mendefinisikan fungsi statis yang dapat dipanggil dari mana saja (termasuk cetak biru). Kode mereka ditunjukkan di bawah ini, Anda dapat mengimplementasikannya dengan cara yang menurut Anda paling baik (mungkin lebih mudah untuk menempatkan mereka di kelas aktor Portal).

 FVector ConvertLocationToActorSpace( FVector Location, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FVector::ZeroVector; } FVector Direction = Location - Reference->GetActorLocation(); FVector TargetLocation = Target->GetActorLocation(); FVector Dots; Dots.X = FVector::DotProduct( Direction, Reference->GetActorForwardVector() ); Dots.Y = FVector::DotProduct( Direction, Reference->GetActorRightVector() ); Dots.Z = FVector::DotProduct( Direction, Reference->GetActorUpVector() ); FVector NewDirection = Dots.X * Target->GetActorForwardVector() + Dots.Y * Target->GetActorRightVector() + Dots.Z * Target->GetActorUpVector(); return TargetLocation + NewDirection; } 

Transformasi di sini dilakukan dengan menghitung produk skalar vektor untuk menentukan sudut yang berbeda. Vektor Direction tidak dinormalisasi, yaitu, kita dapat kembali mengalikan hasil Dots dengan vektor Target untuk mendapatkan posisi pada jarak yang persis sama di ruang lokal aktor Target.

 FRotator ConvertRotationToActorSpace( FRotator Rotation, AActor* Reference, AActor* Target ) { if( Reference == nullptr || Target == nullptr ) { return FRotator::ZeroRotator; } FTransform SourceTransform = Reference->GetActorTransform(); FTransform TargetTransform = Target->GetActorTransform(); FQuat QuatRotation = FQuat( Rotation ); FQuat LocalQuat = SourceTransform.GetRotation().Inverse() * QuatRotation; FQuat NewWorldQuat = TargetTransform.GetRotation() * LocalQuat; return NewWorldQuat.Rotator(); } 

Mengubah transformasi agak sulit untuk diimplementasikan. Pada akhirnya, solusi terbaik ternyata adalah penggunaan angka empat , karena ini jauh lebih akurat daripada bekerja dengan sudut Euler normal dan hanya memerlukan beberapa baris kode. Rotasi dengan angka empat dilakukan menggunakan perkalian, jadi dalam kasus kami, menerapkan Inverse () ke rotasi yang ingin dikonversi, kami akan memindahkannya ke ruang lokal. Selanjutnya, kita hanya perlu melipatgandakannya lagi dengan belokan Target untuk mendapatkan belokan terakhir.

Membuat Portal Mesh


Untuk terlihat cantik dari sudut pandang pemain, sistem portal saya menggunakan jaring khusus. Mesh dibagi menjadi dua bidang yang berbeda:

  • Pesawat 1 : Pesawat utama tempat target render ditampilkan. Pesawat ini memiliki perilaku yang agak tidak biasa, karena tugasnya adalah untuk sedikit menjauh dari pemain saat ia mendekati untuk menghindari kliping oleh kamera. Karena batas-batas pesawat tidak bergerak, tetapi hanya bagian tengahnya yang bergerak, ini memungkinkan pemain untuk melakukan penumpukan pada rendering portal tanpa artefak visual. Tepi di tepi memiliki UV sendiri di bagian atas, sedangkan tepi bagian dalam memiliki UV sendiri di bagian bawah, yang membuatnya mudah untuk menutupi mereka di shader.
  • Pesawat 2 : Pesawat ini hanya digunakan untuk memperpanjang kotak pembatas standar mesh. Normal dari simpul diarahkan ke bawah, sehingga bahkan pada tanah non-planar mesh tidak akan terlihat secara default (karena materi rendering tidak akan dua sisi).


Mengapa menggunakan jaring seperti ini?

Saya memutuskan bahwa "pesawat 1" akan meregang ketika pemain mendekat. Ini memungkinkan pemain untuk tumpang tindih portal dan melewatinya tanpa memotong (memotong). Ini bisa terjadi, misalnya, jika kamera belum melewati bidang portal, tetapi kaki pemain sudah menyentuhnya. Ini memungkinkan Anda untuk tidak memotong pemain dan menggandakan jala di sisi lain.

Tugas "pesawat 2" adalah untuk memperpanjang kotak pembatas standar mesh. Karena "bidang 1" datar, kotak pembatas pada satu sumbu memiliki ketebalan 0, dan jika kamera berada di belakangnya, mesin akan memotongnya (artinya, ia tidak akan membuatnya). Plane 1 memiliki ukuran 128 × 128, sehingga dapat dengan mudah diskalakan menggunakan mesin. Pesawat 2 sedikit lebih besar dan di bawah lantai (di bawah 0).

Setelah membuat mesh, kami cukup mengekspornya dari editor 3D pihak ketiga dan mengimpor ke Unreal. Ini akan digunakan pada langkah selanjutnya.

Membuat Materi Portal


Untuk menampilkan sisi lain portal, kita perlu membuat materi kita sendiri. Buat materi baru di browser konten (Saya menyebutnya MAT_PortalBase ):



Sekarang buka dan buat grafik berikut:


Begini cara kerjanya:

  • FadeColor adalah warna yang akan terlihat melalui portal saat itu sangat jauh. Ini diperlukan karena kami tidak selalu me-render semua portal, jadi kami mengaburkan rendering ketika pemain / kamera berada jauh.
  • Untuk mengetahui seberapa jauh pemain dari portal, saya menentukan jarak antara Posisi Kamera dan Posisi Aktor. Lalu saya membagi jarak dengan nilai maksimum yang ingin saya lakukan perbandingan. Misalnya, jika maksimum yang saya tetapkan adalah 2000, dan jarak ke pemain adalah 1000, maka kita mendapatkan 0,5. Jika pemain lebih jauh, maka saya akan mendapatkan nilai lebih dari 1, jadi saya menggunakan titik saturasi untuk membatasinya. Selanjutnya muncul simpul Smoothstep, yang digunakan untuk skala jarak sebagai gradien dan lebih akurat mengontrol naungan portal. Sebagai contoh, saya ingin ketika pemain dekat, bayangan sepenuhnya hilang.
  • Saya menggunakan perhitungan jarak sebagai nilai saluran alpha untuk simpul Lerp untuk mencampur warna shading dan tekstur yang akan membuat target portal.
  • Akhirnya, saya mengisolasi komponen Y dari koordinat UV untuk membuat topeng yang memungkinkan Anda tahu simpul mana dari mesh akan didorong. Saya kalikan topeng ini dengan jumlah tolakan yang saya butuhkan. Saya menggunakan nilai negatif sehingga ketika normals dari simpul dikalikan dengan simpul, mereka bergerak ke arah yang berlawanan.

Setelah melakukan semua ini, kami membuat materi yang siap pakai.

Membuat Aktor Portal di Cetak Biru


Mari kita buat kelas cetak biru baru yang diwarisi dari aktor Portal. Klik kanan pada browser konten dan pilih kelas Blueprint:


Sekarang masukkan "portal" di bidang pencarian untuk memilih kelas portal:


Buka bluetooth jika belum terbuka. Dalam daftar komponen Anda akan melihat hierarki berikut:


Seperti yang kami harapkan, ada komponen root dan root portal. Mari kita tambahkan komponen jala statis ke PortalRootComponent dan muat jala yang dibuat pada langkah sebelumnya ke dalamnya:




Kami juga menambahkan Kotak Tabrakan, yang akan digunakan untuk menentukan apakah pemain ada di dalam volume portal:



Kotak Tabrakan terletak di bawah komponen adegan yang terkait dengan root utama, dan bukan di bawah root Portal. Saya juga menambahkan ikon (papan iklan) dan komponen panah untuk membuat portal lebih terlihat di tingkat. Tentu saja ini tidak perlu.

Sekarang mari kita mengatur materi dalam cetak biru.

Untuk memulainya, kita membutuhkan dua variabel - satu akan bertipe Aktor dan nama adalah PortalTarget , yang kedua adalah tipe Dynamic Material Instance dan disebut MaterialInstance . PortalTarget akan menjadi referensi ke posisi yang dilihat oleh jendela portal (oleh karena itu, variabelnya umum, dengan ikon mata terbuka) sehingga kita bisa mengubahnya ketika aktor ditempatkan di level. MaterialInstance akan menyimpan tautan ke materi dinamis sehingga di masa mendatang kami dapat menetapkan target render portal dengan cepat.


Kita juga perlu menambahkan node acara kita sendiri. Cara terbaik untuk membuka menu klik kanan di Grafik Acara dan menemukan nama-nama acara:


Dan di sini untuk membuat diagram berikut:


  • Mulai Mainkan : di sini kita memanggil fungsi induk SetTarget () dari portal untuk memberikannya tautan ke aktor, yang nantinya akan digunakan untuk SceneCapture. Kemudian kami membuat Bahan Dinamis baru dan menetapkan nilai variabel MaterialInstance. Dengan materi baru ini, kita dapat menugaskannya ke Komponen Jala Statis. Saya juga memberi bahan tekstur tiruan, tapi ini opsional.
  • Clear RTT : Tujuan dari fitur ini adalah untuk menghapus tekstur Target Render yang ditetapkan untuk materi portal. Ini diluncurkan oleh manajer Portal.
  • Atur RTT : tujuan fungsi ini adalah untuk mengatur bahan target render portal. Ini diluncurkan oleh manajer Portal.

Sejauh ini kita sudah selesai dengan bluetooth, tetapi kita akan kembali lagi nanti untuk mengimplementasikan fungsi Tick.

Manajer portal


Jadi, sekarang kita memiliki semua elemen dasar yang diperlukan untuk membuat kelas baru yang diwarisi dari AActor, yang akan menjadi Portal Manager. Anda mungkin tidak memerlukan kelas Portal Manajer di proyek Anda, tetapi dalam kasus saya, ini sangat menyederhanakan bekerja dengan beberapa aspek. Berikut adalah daftar tugas yang dilakukan oleh manajer Portal:

  • Manajer Portal adalah aktor yang dibuat oleh Kontroler Player dan dilampirkan padanya untuk melacak keadaan dan evolusi pemain dalam level game.
  • Buat dan hancurkan render target portal . Idenya adalah untuk secara dinamis membuat tekstur target render yang cocok dengan resolusi layar pemain. Selain itu, ketika mengubah resolusi selama permainan, manajer akan secara otomatis mengonversinya ke ukuran yang diinginkan.
  • Manajer Portal menemukan dan memperbarui tingkat aktor Portal untuk memberi mereka target render. Tugas ini dilakukan sedemikian rupa untuk memastikan kompatibilitas dengan streaming level. Ketika aktor baru muncul, ia harus mendapatkan tekstur. Selain itu, jika target Render berubah, manajer juga dapat menetapkan yang baru secara otomatis. Ini membuatnya lebih mudah untuk mengelola sistem, daripada meminta setiap aktor Portal menghubungi manajer secara manual.
  • Komponen SceneCapture dilampirkan ke manajer Portal, agar tidak membuat satu salinan untuk setiap portal. Selain itu, ini memungkinkan Anda untuk menggunakannya kembali setiap kali kami beralih ke aktor portal tertentu di tingkat tersebut.
  • Ketika portal memutuskan untuk memindahkan pemain, ia mengirim permintaan ke Portal Manager. Ini diperlukan untuk memperbarui portal sumber dan tujuan (jika ada), sehingga transisi terjadi tanpa sambungan.
  • Manajer Portal diperbarui pada akhir fungsi Character tick () sehingga semuanya diperbarui dengan benar, termasuk kamera pemain. Ini memastikan bahwa segala sesuatu di layar disinkronkan dan menghindari penundaan satu frame selama rendering oleh mesin.

Mari kita lihat header Portal Manager:

 #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h" //Forward declaration class AExedrePlayerController; class AExedrePortal; class UExedreScriptedTexture; UCLASS() class EXEDRE_API AExedrePortalManager : public AActor { GENERATED_UCLASS_BODY() public: AExedrePortalManager(); //Called by a Portal actor when wanting to teleport something UFUNCTION(BlueprintCallable, Category="Portal") void RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ); //Save a reference to the PlayerControler void SetControllerOwner( AExedrePlayerController* NewOwner ); //Various setup that happens during spawn void Init(); //Manual Tick void Update( float DeltaTime ); //Find all the portals in world and update them //returns the most valid/usable one for the Player AExedrePortal* UpdatePortalsInWorld(); //Update SceneCapture void UpdateCapture( AExedrePortal* Portal ); //Accessor for Debug purpose UTexture* GetPortalTexture(); //Accessor for Debug purpose FTransform GetCameraTransform(); private: //Function to create the Portal render target void GeneratePortalTexture(); UPROPERTY() USceneCaptureComponent2D* SceneCapture; //Custom class, can be replaced by a "UCanvasRenderTarget2D" instead //See : https://api.unrealengine.com/INT/API/Runtime/Engine/Engine/UCanvasRenderTarget2D/index.html UPROPERTY() UExedreScriptedTexture* PortalTexture; UPROPERTY() AExedrePlayerController* ControllerOwner; int32 PreviousScreenSizeX; int32 PreviousScreenSizeY; float UpdateDelay; }; 



Sebelum masuk ke detail, saya akan menunjukkan bagaimana seorang aktor dibuat dari kelas Player Controller, dipanggil dari fungsi BeginPlay ():

  FActorSpawnParameters SpawnParams; PortalManager = nullptr; PortalManager = GetWorld()->SpawnActor<AExedrePortalManager>( AExedrePortalManager::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, SpawnParams); PortalManager->AttachToActor( this, FAttachmentTransformRules::SnapToTargetIncludingScale); PortalManager->SetControllerOwner( this ); PortalManager->Init(); 

Jadi, kami membuat aktor, melampirkannya ke controller pemain (ini), dan kemudian menyimpan tautan dan memanggil fungsi Init ().

Penting juga untuk dicatat bahwa kami memperbarui aktor secara manual dari kelas Karakter:

 void AExedreCharacter::TickActor( float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction ) { Super::TickActor( DeltaTime, TickType, ThisTickFunction ); if( UGameplayStatics::GetPlayerController(GetWorld(), 0) != nullptr ) { AExedrePlayerController* EPC = Cast<AExedrePlayerController>( UGameplayStatics::GetPlayerController(GetWorld(), 0) ); EPC->PortalManager->Update( DeltaTime ); } } 

Dan inilah konstruktor dari Portal Manager. Perhatikan bahwa Centang dinonaktifkan, lagi karena kami akan memperbarui Portal Manager secara manual melalui pemain.

 AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; } 



Berikut adalah fungsi get / set Portal Manager (setelah itu kami akan beralih ke hal-hal yang lebih menarik):

 void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() { //Portal Texture is a custom component class that embed a UCanvasRenderTraget2D //The GetTexture() simply returns the RenderTarget contained in that class. //IsValidLowLevel() is used here as a way to ensure the Texture has not been destroyed or garbage collected. if( PortalTexture != nullptr && PortalTexture->IsValidLowLevel() ) { return PortalTexture->GetTexture(); } else { return nullptr; } } 



Jelas, hal pertama yang harus dimulai adalah fungsi Init () .

Tujuan utama dari fungsi ini adalah untuk membuat komponen SceneCapture (yaitu, perangkat penangkapan yang disebutkan di atas) dan mengkonfigurasinya dengan benar. Itu dimulai dengan penciptaan objek baru dan pendaftarannya sebagai komponen aktor ini. Kemudian kami beralih ke pengaturan properti yang terkait dengan tangkapan ini.

Properti menyebutkan:

  • bCaptureEveryFrame = false : kami tidak ingin tangkapan diaktifkan ketika kami tidak membutuhkannya. Kami akan mengelolanya secara manual.
  • bEnableClipPlane = true : Properti yang cukup penting untuk rendering portal capture dengan benar.
  • bUseCustomProjectionMatrix = true : ini memungkinkan kita untuk mengganti proyeksi Capture dengan proyeksi kita sendiri, berdasarkan sudut pandang pemain.
  • CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : Mode ini sedikit mahal, tetapi perlu untuk memberikan informasi yang cukup.

Properti yang tersisa terutama terkait dengan parameter pasca pemrosesan. Mereka adalah cara yang nyaman untuk mengontrol kualitas, dan karenanya menangkap kinerja.

Bagian terakhir memanggil fungsi yang membuat Target Render, yang akan kita lihat di bawah.

 void AExedrePortalManager::Init() { //------------------------------------------------ //Create Camera //------------------------------------------------ SceneCapture = NewObject<USceneCaptureComponent2D>(this, USceneCaptureComponent2D::StaticClass(), *FString("PortalSceneCapture")); SceneCapture->AttachToComponent( GetRootComponent(), FAttachmentTransformRules::SnapToTargetIncludingScale ); SceneCapture->RegisterComponent(); SceneCapture->bCaptureEveryFrame = false; SceneCapture->bCaptureOnMovement = false; SceneCapture->LODDistanceFactor = 3; //Force bigger LODs for faster computations SceneCapture->TextureTarget = nullptr; SceneCapture->bEnableClipPlane = true; SceneCapture->bUseCustomProjectionMatrix = true; SceneCapture->CaptureSource = ESceneCaptureSource::SCS_SceneColorSceneDepth; //Setup Post-Process of SceneCapture (optimization : disable Motion Blur, etc) FPostProcessSettings CaptureSettings; CaptureSettings.bOverride_AmbientOcclusionQuality = true; CaptureSettings.bOverride_MotionBlurAmount = true; CaptureSettings.bOverride_SceneFringeIntensity = true; CaptureSettings.bOverride_GrainIntensity = true; CaptureSettings.bOverride_ScreenSpaceReflectionQuality = true; CaptureSettings.AmbientOcclusionQuality = 0.0f; //0=lowest quality..100=maximum quality CaptureSettings.MotionBlurAmount = 0.0f; //0 = disabled CaptureSettings.SceneFringeIntensity = 0.0f; //0 = disabled CaptureSettings.GrainIntensity = 0.0f; //0 = disabled CaptureSettings.ScreenSpaceReflectionQuality = 0.0f; //0 = disabled CaptureSettings.bOverride_ScreenPercentage = true; CaptureSettings.ScreenPercentage = 100.0f; SceneCapture->PostProcessSettings = CaptureSettings; //------------------------------------------------ //Create RTT Buffer //------------------------------------------------ GeneratePortalTexture(); } 



GeneratePortalTexture () adalah fungsi yang dipanggil saat diperlukan ketika Anda perlu membuat tekstur Target Render baru untuk portal. Ini terjadi pada fungsi inisialisasi, tetapi juga dapat dipanggil selama peningkatan Portal Manager. Itulah sebabnya fungsi ini memiliki pemeriksaan internal untuk mengubah resolusi viewport. Jika itu tidak terjadi, maka pembaruan tidak dilakukan.

Dalam kasus saya, saya membuat kelas pembungkus untuk UCanvasRenderTarget2D. Saya menyebutnya ExedreScriptedTexture, itu adalah komponen yang dapat dilampirkan ke aktor. Saya membuat kelas ini untuk mengelola target render dengan mudah dengan aktor yang memiliki tugas rendering. Dia melakukan inisialisasi yang tepat dari Target Render dan kompatibel dengan sistem UI saya sendiri. Namun, dalam konteks portal, tekstur RenderTarget2D reguler lebih dari cukup. Karena itu, Anda cukup menggunakannya.

 void AExedrePortalManager::GeneratePortalTexture() { int32 CurrentSizeX = 1920; int32 CurrentSizeY = 1080; if( ControllerOwner != nullptr ) { ControllerOwner->GetViewportSize(CurrentSizeX, CurrentSizeY); } CurrentSizeX = FMath::Clamp( int(CurrentSizeX / 1.7), 128, 1920); //1920 / 1.5 = 1280 CurrentSizeY = FMath::Clamp( int(CurrentSizeY / 1.7), 128, 1080); if( CurrentSizeX == PreviousScreenSizeX && CurrentSizeY == PreviousScreenSizeY ) { return; } PreviousScreenSizeX = CurrentSizeX; PreviousScreenSizeY = CurrentSizeY; //Cleanup existing RTT if( PortalTexture != nullptr && PortalTexture->IsValidLowLevel() ) { PortalTexture->DestroyComponent(); GEngine->ForceGarbageCollection(); } //Create new RTT PortalTexture = nullptr; PortalTexture = NewObject<UExedreScriptedTexture>(this, UExedreScriptedTexture::StaticClass(), *FString("PortalRenderTarget")); PortalTexture->SizeX = CurrentSizeX; PortalTexture->SizeY = CurrentSizeY; //Custom properties of the UExedreScriptedTexture class PortalTexture->Gamma = 1.0f; PortalTexture->WrapModeX = 1; //Clamp PortalTexture->WrapModeY = 1; //Clamp PortalTexture->bDrawWidgets = false; PortalTexture->bGenerateMipMaps = false; PortalTexture->SetClearOnUpdate( false ); //Will be cleared by SceneCapture instead PortalTexture->Format = ERenderTargetFormat::RGBA16; //Needs 16b to get >1 for Emissive PortalTexture->AttachToComponent( GetRootComponent(), FAttachmentTransformRules::SnapToTargetIncludingScale ); PortalTexture->RegisterComponent(); PortalTexture->SetOwner( this ); PortalTexture->Init(); PortalTexture->SetFilterMode( TextureFilter::TF_Bilinear ); } 

Seperti disebutkan di atas, saya membuat kelas saya sendiri, jadi properti yang ditetapkan di sini harus disesuaikan dengan Target Render yang biasa.

Penting untuk memahami di mana tangkapan akan ditampilkan. Karena target render akan ditampilkan dalam game, ini berarti bahwa ini akan terjadi sebelum seluruh pasca pemrosesan, dan oleh karena itu kita perlu membuat adegan dengan informasi yang cukup (untuk menyimpan nilai di atas 1 untuk membuat Bloom). Itu sebabnya saya memilih format RGBA16 (perhatikan bahwa ia memiliki Enum sendiri, Anda harus menggunakan ETextureRenderTargetFormat sebagai gantinya).

Untuk informasi lebih lanjut, lihat sumber-sumber berikut:




Selanjutnya kami akan mempertimbangkan fungsi pembaruan. Fungsi dasarnya cukup sederhana dan menyebabkan lebih kompleks. Ada penundaan sebelum memanggil fungsi GeneratePortalTexture () untuk menghindari membuat ulang target render saat mengubah ukuran viewport (misalnya, di editor). Selama publikasi game, penundaan ini dapat dihapus.

 void AExedrePortalManager::Update( float DeltaTime ) { //----------------------------------- //Generate Portal texture ? //----------------------------------- UpdateDelay += DeltaTime; if( UpdateDelay > 1.0f ) { UpdateDelay = 0.0f; GeneratePortalTexture(); } //----------------------------------- //Find portals in the level and update them //----------------------------------- AExedrePortal* Portal = UpdatePortalsInWorld(); if( Portal != nullptr ) { UpdateCapture( Portal ); } } 

Kami memanggil UpdatePortalsInWorld () untuk menemukan semua portal yang ada di dunia saat ini (termasuk semua level yang dimuat) dan memperbaruinya. Fungsi ini juga menentukan yang mana "aktif", yaitu terlihat oleh pemain. Jika kami menemukan portal yang aktif, kami memanggil UpdateCapture () , yang mengontrol komponen SceneCapture.



Berikut cara kerja pembaruan dunia di dalam UpdatePortalsInWorld () :

  1. ( )
  2. iterator ,
  3. , , ClearRTT() , . (, ).
  4. , , , , .

Pemeriksaan yang menentukan kebenaran portal itu sederhana: kami memberikan prioritas ke portal yang paling dekat dengan pemain, karena ia kemungkinan besar akan menjadi yang paling terlihat dari sudut pandangnya. Untuk menjatuhkan kerabat, tetapi, misalnya, portal yang terletak di belakang pemain, diperlukan pemeriksaan yang lebih kompleks, tetapi saya tidak ingin fokus pada tutorial ini, karena ini bisa menjadi sangat sulit.

 AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter(); //----------------------------------- //Update Portal actors in the world (and active one if nearby) //----------------------------------- AExedrePortal* ActivePortal = nullptr; FVector PlayerLocation = Character->GetActorLocation(); FVector CameraLocation = Character->GetCameraComponent()->GetComponentLocation(); float Distance = 4096.0f; for( TActorIterator<AExedrePortal>ActorItr( GetWorld() ); ActorItr; ++ActorItr ) { AExedrePortal* Portal = *ActorItr; FVector PortalLocation = Portal->GetActorLocation(); FVector PortalNormal = -1 * Portal->GetActorForwardVector(); //Reset Portal Portal->ClearRTT(); Portal->SetActive( false ); //Find the closest Portal when the player is Standing in front of float NewDistance = FMath::Abs( FVector::Dist( PlayerLocation, PortalLocation ) ); if( NewDistance < Distance ) { Distance = NewDistance; ActivePortal = Portal; } } return ActivePortal; } 



Saatnya untuk mempertimbangkan fungsi UpdateCapture () .

Ini adalah fitur pemutakhiran yang menangkap sisi lain portal. Dari komentar semuanya harus jelas, tetapi di sini adalah deskripsi singkat:

  1. Kami mendapatkan tautan ke Character and Player Controller.
  2. Kami memeriksa apakah semuanya sudah benar (Portal, komponen SceneCapture, Player).
  3. Camera Target .
  4. , SceneCapture.
  5. SceneCapture Target.
  6. , SceneCapure , , .
  7. Render Target SceneCapture, .
  8. PlayerController.
  9. , Capture SceneCapture .

Seperti yang dapat kita lihat, saat memindahkan pemain, elemen kunci dari perilaku alami dan tanpa cela dari SceneCapture adalah transformasi yang benar dari posisi dan rotasi portal ke ruang Target lokal.

Untuk definisi ConvertLocationToActorSpace (), lihat “Teleporting an Actor”.

 void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter(); //----------------------------------- //Update SceneCapture (discard if there is no active portal) //----------------------------------- if(SceneCapture != nullptr && PortalTexture != nullptr && Portal != nullptr && Character != nullptr ) { UCameraComponent* PlayerCamera = Character->GetCameraComponent(); AActor* Target = Portal->GetTarget(); //Place the SceneCapture to the Target if( Target != nullptr ) { //------------------------------- //Compute new location in the space of the target actor //(which may not be aligned to world) //------------------------------- FVector NewLocation = UTool::ConvertLocationToActorSpace( PlayerCamera->GetComponentLocation(), Portal, Target ); SceneCapture->SetWorldLocation( NewLocation ); //------------------------------- //Compute new Rotation in the space of the //Target location //------------------------------- FTransform CameraTransform = PlayerCamera->GetComponentTransform(); FTransform SourceTransform = Portal->GetActorTransform(); FTransform TargetTransform = Target->GetActorTransform(); FQuat LocalQuat = SourceTransform.GetRotation().Inverse() * CameraTransform.GetRotation(); FQuat NewWorldQuat = TargetTransform.GetRotation() * LocalQuat; //Update SceneCapture rotation SceneCapture->SetWorldRotation( NewWorldQuat ); //------------------------------- //Clip Plane : to ignore objects between the //SceneCapture and the Target of the portal //------------------------------- SceneCapture->ClipPlaneNormal = Target->GetActorForwardVector(); SceneCapture->ClipPlaneBase = Target->GetActorLocation() + (SceneCapture->ClipPlaneNormal * -1.5f); //Offset to avoid visible pixel border } //Switch on the valid Portal Portal->SetActive( true ); //Assign the Render Target Portal->SetRTT( PortalTexture->GetTexture() ); SceneCapture->TextureTarget = PortalTexture->GetTexture(); //Get the Projection Matrix SceneCapture->CustomProjectionMatrix = ControllerOwner->GetCameraProjectionMatrix(); //Say Cheeeeese ! SceneCapture->CaptureScene(); } } 

Fungsi GetCameraProjectionMatrix () tidak ada secara default di kelas PlayerController, saya menambahkannya sendiri. Itu ditunjukkan di bawah ini:

 FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; } 



Akhirnya, kita perlu mengimplementasikan panggilan ke fungsi Teleport. Alasan pemrosesan sebagian dari teleportasi melalui Manajer Portal adalah bahwa perlu untuk menjamin pembaruan portal yang diperlukan, karena hanya Manajer yang memiliki informasi tentang semua portal di tempat kejadian.

Jika kita memiliki dua portal yang terhubung, maka ketika berpindah dari satu portal ke portal lainnya, kita perlu memperbarui keduanya dalam satu Tick. Jika tidak, pemain akan berteleportasi dan akan berada di sisi lain portal, tetapi Target Portal tidak akan aktif sampai frame / ukuran berikutnya. Ini akan membuat celah visual dengan bahan offset dari bidang jala yang kita lihat di atas.

 void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport ); //----------------------------------- //Force update //----------------------------------- AExedrePortal* FuturePortal = UpdatePortalsInWorld(); if( FuturePortal != nullptr ) { FuturePortal->ForceTick(); //Force update before the player render its view since he just teleported UpdateCapture( FuturePortal ); } } } 

Yah, begitulah, akhirnya kami selesai dengan Portal Manager!

Selesaikan cetak biru itu


Setelah menyelesaikan Portal Manager, kita hanya perlu menyelesaikan aktor Portal itu sendiri, setelah itu sistem akan bekerja. Satu-satunya hal yang hilang di sini adalah fitur Centang:


Begini cara kerjanya:

  • Kami memperbarui Materi sehingga tidak tetap dalam kondisi aktif.
  • Jika portal saat ini tidak aktif , sisa ukuran akan dibuang.
  • Kami mendapatkan kelas Karakter untuk mengakses Lokasi Kamera .
  • Bagian pertama memeriksa apakah kamera berada di kotak tumbukan portal. Jika demikian, maka kami mengimbangi portal mesh dengan Materialnya .
  • Bagian kedua adalah memeriksa ulang lokasi di dalam kotak tumbukan. Jika dijalankan, maka kami memanggil fungsi yang memeriksa apakah kami melewati portal .
  • , Portal manager, Teleport .

Dalam tangkapan layar grafik saya, Anda dapat melihat dua poin menarik: Is Point Inside Box dan Get Portal Manager . Saya belum menjelaskan kedua fungsi ini. Ini adalah fungsi statis yang saya definisikan di kelas saya sendiri sehingga Anda dapat memanggilnya dari mana saja. Ini adalah semacam kelas pembantu. Kode fungsi-fungsi ini ditunjukkan di bawah ini, Anda sendiri dapat memutuskan di mana memasukkannya. Jika Anda tidak membutuhkannya di luar sistem portal, Anda dapat memasukkannya langsung ke kelas aktor Portal.

Awalnya saya ingin menggunakan sistem tumbukan untuk menemukan aktor portal di dalam kotak tumbukan, tetapi bagi saya sepertinya tidak cukup dapat diandalkan. Selain itu, menurut saya metode ini lebih cepat digunakan dan memiliki kelebihan: memperhitungkan rotasi aktor.

 bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) { //From : //https://stackoverflow.com/questions/52673935/check-if-3d-point-inside-a-box/52674010 FVector Center = Box->GetComponentLocation(); FVector Half = Box->GetScaledBoxExtent(); FVector DirectionX = Box->GetForwardVector(); FVector DirectionY = Box->GetRightVector(); FVector DirectionZ = Box->GetUpVector(); FVector Direction = Point - Center; bool IsInside = FMath::Abs( FVector::DotProduct( Direction, DirectionX ) ) <= Half.X && FMath::Abs( FVector::DotProduct( Direction, DirectionY ) ) <= Half.Y && FMath::Abs( FVector::DotProduct( Direction, DirectionZ ) ) <= Half.Z; return IsInside; } else { return false; } } 

 AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr; //Retrieve the World from the Context actor if( Context != nullptr && Context->GetWorld() != nullptr ) { //Find PlayerController AExedrePlayerController* EPC = Cast<AExedrePlayerController>( Context->GetWorld()->GetFirstPlayerController() ); //Retrieve the Portal Manager if( EPC != nullptr && EPC->GetPortalManager() != nullptr ) { Manager = EPC->GetPortalManager(); } } return Manager; } 



Bagian terakhir dari aktor Blueprint adalah ForceTick . Ingatlah bahwa Force Tick dipanggil ketika pemain melintasi portal dan di sebelah portal lain yang Portal Manajernya memaksakan pembaruan. Karena kami baru saja teleportasi, tidak perlu menggunakan kode yang sama, dan Anda dapat menggunakan versi yang disederhanakan:


Proses dimulai kira-kira bersamaan dengan fungsi Tick, tetapi kami hanya menjalankan bagian pertama dari urutan, yang memperbarui materi.

Apakah kita sudah selesai?


Hampir.

Jika kami menerapkan sistem portal dalam bentuk ini, maka kemungkinan besar kami akan menghadapi masalah berikut:


Apa yang sedang terjadi di sini?

Dalam gif ini, frame rate game dibatasi hingga 6 FPS untuk menunjukkan masalahnya dengan lebih jelas. Dalam satu bingkai, kubus menghilang karena sistem kliping Unreal Engine menganggapnya tidak terlihat.

Ini karena penemuan dilakukan dalam bingkai saat ini, dan kemudian digunakan di berikutnya. Ini menciptakan penundaan satu frame . Ini biasanya dapat diatasi dengan memperluas kotak pembatas objek sehingga terdaftar sebelum terlihat. Namun, ini tidak akan berfungsi di sini, karena ketika kita melintasi portal, kita berpindah dari satu tempat ke tempat yang sama sekali berbeda.

Menonaktifkan sistem kliping juga tidak mungkin, terutama karena pada level dengan banyak objek ini akan mengurangi kinerja. Selain itu, saya mencoba banyak tim dari mesin Unreal, tetapi tidak mendapatkan hasil positif: dalam semua kasus, penundaan satu frame tetap. Untungnya, setelah mempelajari secara terperinci kode sumber Unreal Engine, saya berhasil menemukan solusinya (jalurnya panjang - butuh lebih dari seminggu)!

Seperti halnya komponen SceneCapture, Anda dapat memberi tahu kamera pemain bahwa kami melakukan cut jump- posisi kamera melompati dua frame, yang berarti kita tidak bisa mengandalkan informasi dari frame sebelumnya. Perilaku ini dapat diamati ketika menggunakan Matinee atau Sequencer, misalnya, ketika mengganti kamera: blur atau smoothing tidak dapat bergantung pada informasi dari frame sebelumnya.

Untuk melakukan ini, kita perlu mempertimbangkan dua aspek:

  • LocalPlayer : kelas ini memproses berbagai informasi (misalnya, viewport pemain) dan dikaitkan dengan PlayerController. Di sinilah kita dapat memengaruhi proses rendering kamera pemain.
  • PlayerController : ketika seorang pemain teleport, kelas ini mulai splicing berkat akses ke LocalPlayer.

Keuntungan besar dari solusi ini adalah bahwa intervensi dalam proses rendering mesin minimal dan mudah dirawat di masa depan pembaruan Unreal Engine.



Mari kita mulai dengan membuat kelas baru yang diwarisi dari LocalPlayer. Di bawah ini adalah tajuk yang mengidentifikasi dua komponen utama: mendefinisikan kembali perhitungan Scene Viewport dan fungsi baru untuk mengaktifkan pelekatan kamera.

 #pragma once #include "CoreMinimal.h" #include "Engine/LocalPlayer.h" #include "ExedreLocalPlayer.generated.h" UCLASS() class EXEDRE_API UExedreLocalPlayer : public ULocalPlayer { GENERATED_BODY() UExedreLocalPlayer(); public: FSceneView* CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) override; void PerformCameraCut(); private: bool bCameraCut; }; 

Begini cara semuanya diterapkan:

 #include "Exedre.h" #include "ExedreLocalPlayer.h" UExedreLocalPlayer::UExedreLocalPlayer() { bCameraCut = false; } FSceneView* UExedreLocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily, FVector& OutViewLocation, FRotator& OutViewRotation, FViewport* Viewport, class FViewElementDrawer* ViewDrawer, EStereoscopicPass StereoPass) { // ULocalPlayer::CalcSceneView() use a ViewInitOptions to create // a FSceneView which contains a "bCameraCut" variable // See : H:\GitHub\UnrealEngine\Engine\Source\Runtime\Renderer\Private\SceneCaptureRendering.cpp // as well for bCameraCutThisFrame in USceneCaptureComponent2D FSceneView* View = Super::CalcSceneView(ViewFamily, OutViewLocation, OutViewRotation, Viewport, ViewDrawer, StereoPass ); if( bCameraCut ) { View->bCameraCut = true; bCameraCut = false; } return View; } void UExedreLocalPlayer::PerformCameraCut() { bCameraCut = true; } 

PerformCameraCut () baru memulai Camera Cut dengan nilai boolean. Ketika mesin memanggil fungsi CalcSceneView () , pertama-tama kita menjalankan fungsi aslinya. Kemudian kita periksa, kita perlu melakukan perekatan. Jika demikian, kita mendefinisikan ulang variabel Camera Cut Boolean di dalam struktur FSceneView , yang akan digunakan oleh proses rendering mesin, dan kemudian mengatur ulang variabel Boolean (menggunakannya).



Di sisi Pengontrol Pemain, perubahannya minimal. Anda perlu menambahkan variabel ke header untuk menyimpan tautan ke kelas asli LocalPlayer:

  UPROPERTY() UExedreLocalPlayer* LocalPlayer; 

Kemudian dalam fungsi BeginPlay () :

  LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() ); 

Saya juga menambahkan fungsi untuk meluncurkan Cut dengan cepat:

 void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } } 



Akhirnya, dalam fungsi Portal Manager RequestTeleportByPortal (), kita dapat mengeksekusi selama teleportasi Cut Kamera:

 void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...] 

Dan itu saja!

Potongan Kamera harus dipanggil sebelum SceneCapture diperbarui, itulah sebabnya ia ada di awal fungsi.

Hasil akhir


Sekarang kita telah belajar berpikir di portal.

Jika sistem bekerja dengan baik, maka kita harus dapat membuat hal-hal ini:


Jika Anda mengalami masalah, periksa hal-hal berikut:

  • Verifikasi bahwa Portal Manager dibuat dan diinisialisasi dengan benar.
  • Target rendering dibuat dengan benar (Anda dapat menggunakan target yang dibuat di browser konten untuk memulai).
  • Portal diaktifkan dan dinonaktifkan dengan benar.
  • Portal memiliki aktor Target yang ditetapkan dengan benar di editor.

Tanya Jawab


Pertanyaan paling populer yang saya tanyakan tentang tutorial ini:

Apakah mungkin untuk mengimplementasikan ini pada tumpul, dan tidak melalui C ++?

Sebagian besar kode dapat diimplementasikan dalam tumpul, dengan pengecualian dua aspek:

  • Fungsi LocalPlayer GetProjectionData () yang digunakan untuk mendapatkan matriks proyeksi tidak tersedia dalam cetak biru.
  • Fungsi LocalPlayer CalcSceneView () , yang sangat penting untuk menyelesaikan masalah sistem kliping, tidak tersedia dalam cetak biru.

Oleh karena itu, Anda perlu menggunakan implementasi C ++ untuk mengakses kedua fungsi ini, atau memodifikasi kode sumber mesin agar dapat diakses melalui cetak biru.

Bisakah saya menggunakan sistem ini di VR?

Ya, sebagian besar. Namun, beberapa bagian harus disesuaikan, misalnya:

  • Anda perlu menggunakan dua Target Render (satu untuk setiap mata) dan menutupi mereka di material portal untuk ditampilkan berdampingan di ruang layar. Setiap target render harus setengah lebar dari resolusi perangkat VR.
  • Anda perlu menggunakan dua SceneCapture untuk membuat target dengan jarak yang benar (jarak antara mata) untuk membuat efek stereoskopik.

Masalah utama adalah kinerja, karena sisi lain dari portal harus dirender dua kali.

Bisakah objek lain melewati portal?

Tidak ada dalam kode saya. Namun, membuatnya lebih umum tidak begitu sulit. Untuk melakukan ini, portal perlu melacak lebih banyak informasi tentang semua objek terdekat untuk memeriksa apakah mereka melintasinya.

Apakah sistem mendukung rekursi (portal di dalam portal)?

Tutorial ini bukan. Untuk rekursi, Anda memerlukan target render tambahan dan SceneCapture. Anda juga perlu menentukan RenderTarget mana yang akan di-render terlebih dahulu, dan seterusnya. Ini cukup sulit dan saya tidak ingin melakukan ini, karena untuk proyek saya ini tidak perlu.

Bisakah saya melewati portal di dekat tembok?

Sayangnya tidak. Namun, saya melihat dua cara untuk mengimplementasikan ini (secara teoritis):

  • Nonaktifkan tabrakan pemain sehingga ia dapat melewati dinding. Mudah diterapkan, tetapi akan menimbulkan banyak efek samping.
  • Retas sistem tabrakan untuk membuat lubang secara dinamis, yang akan memungkinkan pemain untuk melewatinya. Untuk melakukan ini, Anda perlu memodifikasi sistem fisik mesin. Namun, dari apa yang saya ketahui, setelah memuat level, fisika statis tidak dapat diperbarui. Karena itu, untuk mendukung fitur ini akan membutuhkan banyak pekerjaan. Jika portal Anda statis, maka Anda mungkin dapat mengatasi masalah ini dengan menggunakan streaming level untuk beralih di antara berbagai tabrakan.

Source: https://habr.com/ru/post/id448802/


All Articles