Dans cet article, je vais vous expliquer comment créer des portails dans Unreal Engine 4. Je n'ai trouvé aucune source décrivant un tel système en détail (surveillance via des portails et passage à travers eux), j'ai donc décidé d'écrire le mien.
Qu'est-ce qu'un portail?
Commençons par des exemples et des explications sur ce qu'est un portail. La façon la plus simple de décrire les portails comme un moyen de passage d'un espace à un autre. Dans certains jeux populaires, ce concept est utilisé pour les effets visuels et même pour les mécaniques de jeu:
Exemples de portail de jeu (GIF)Antichamber (2013) et Portal (2007)Proie, 2006 Des trois jeux, le plus célèbre est probablement Portal, mais personnellement j'ai toujours admiré Prey et c'est elle que je voulais copier. Une fois, j'ai essayé d'implémenter ma propre version dans Unreal Engine 4, mais je n'ai pas vraiment réussi, car le moteur manquait de fonctionnalités. Néanmoins, j'ai réussi à mener ces expériences:
Cependant, ce n'est que dans les nouvelles versions d'Unreal Engine que j'ai finalement réussi à obtenir l'effet souhaité:
Portails - comment fonctionnent-ils?
Avant de poursuivre avec les détails, regardons l'image générale du fonctionnement des portails.
En fait, un portail est une fenêtre qui ne va pas à l'extérieur, mais vers un autre endroit, c'est-à-dire que nous définissons localement un point de vue spécifique par rapport à l'objet et reproduisons ce point de vue ailleurs. En utilisant ce principe, nous pouvons relier deux espaces, même s'ils sont très éloignés l'un de l'autre. La fenêtre ressemble à un masque qui nous permet de savoir où et quand afficher un autre espace au lieu de l'original. Puisque le point de départ de la vue est reproduit ailleurs, cela nous donne l'illusion de la continuité.

Dans cette image, le périphérique de capture (SceneCapture dans UE4) est situé devant l'espace qui correspond à l'espace vu du point de vue du joueur. Tout ce qui est visible après la ligne est remplacé par ce que la capture peut voir. Étant donné que le dispositif de capture peut être situé entre la porte et d'autres objets, il est important d'utiliser le soi-disant «plan de détourage». Dans le cas du portail, nous voulons que le plan de détourage proche masque les objets visibles devant le portail.
Pour résumer. Nous avons besoin de:
- Emplacement du joueur
- Point d'entrée du portail
- Point de sortie du portail
- Dispositif de détourage avec plan de détourage
Comment implémenter cela dans Unreal Engine?
J'ai construit mon système sur la base de deux classes principales gérées par
PlayerController et
Character . La classe
Portal est un véritable point d'entrée de portail, dont le point de vue / sortie est l'acteur cible. Il existe également un
gestionnaire de portail , qui est généré par le PlayerController et mis à jour par le personnage pour gérer chaque portail au niveau et les mettre à jour, ainsi que pour manipuler l'objet SceneCapture (qui est commun à tous les portails).
Gardez à l'esprit que le didacticiel s'attend à ce que vous ayez accès aux classes Character et PlayerController à partir du code. Dans mon cas, ils s'appellent ExedreCharacter et ExedrePlayerController.
Création d'une classe d'acteur de portail
Commençons par l'acteur du portail, qui sera utilisé pour définir les «fenêtres» à travers lesquelles nous regarderons le niveau. La tâche de l'acteur est de fournir des informations sur le joueur pour calculer différentes positions et virages. Il sera également engagé à reconnaître si le joueur traverse le portail et sa téléportation.
Avant de commencer une discussion détaillée sur l'acteur, permettez-moi d'expliquer quelques concepts que j'ai créés pour gérer le système de portail:
- Pour un refus commode des calculs, le portail a un statut actif-inactif. Cet état est mis à jour par Portal Manager.
- Le portail a des côtés avant et arrière déterminés par sa position et sa direction (vecteur avant).
- Pour savoir si le joueur traverse le portail, il enregistre la position précédente du joueur et la compare avec la position actuelle. Si dans la mesure précédente le joueur était devant le portail, et dans le courant - derrière lui, alors nous pensons que le joueur l'a traversé. Le comportement inverse est ignoré.
- Le portail a un volume limité, afin de ne pas effectuer de calculs et de vérifications tant que le joueur n'est pas dans ce volume. Exemple: Ignorez l'intersection si le joueur ne touche pas réellement le portail.
- La position du joueur est calculée à partir de la position de la caméra pour garantir un comportement correct lorsque le point de vue traverse le portail mais pas le corps du joueur.
- Le portail reçoit une cible de rendu, qui affiche un point de vue différent dans chaque mesure au cas où la texture la prochaine fois est incorrecte et doit être remplacée.
- Le portail stocke un lien vers un autre acteur appelé Target, afin de savoir où l'autre espace doit être contacté.
En utilisant ces règles, j'ai créé une nouvelle classe ExedrePortal héritée d'AActor comme point de départ. Voici son titre:
#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;
Comme vous pouvez le voir, il existe la plupart des comportements décrits ici. Voyons maintenant comment ils sont traités dans le corps (.cpp).
Le concepteur prépare ici les composants racine. J'ai décidé de créer deux composants racine, car l'acteur du portail combinera à la fois les effets graphiques et les collisions / reconnaissance. J'avais donc besoin d'un moyen simple pour déterminer où se trouve le plan de la fenêtre / du portail, sans avoir besoin de fonctionnalités Bluetooth ou d'autres astuces. PortalRootComponent sera la base de tous les calculs liés au portail.
La racine du portail est définie sur dynamique, au cas où la classe Blueprint l'animerait (par exemple, utiliser une animation d'ouverture / fermeture).
Il n'y a que les fonctions Get et Set, et rien de plus. Nous gérerons l'état d'activité à partir d'un autre endroit.
bool AExedrePortal::IsActive() { return bIsActive; } void AExedrePortal::SetActive( bool NewActive ) { bIsActive = NewActive; }
Événements Blueprint, je ne fais rien dans la classe C ++.
void AExedrePortal::ClearRTT_Implementation() { } void AExedrePortal::SetRTT_Implementation( UTexture* RenderTexture ) { } void AExedrePortal::ForceTick_Implementation() { }
Les fonctions Get et Set pour l'acteur cible. Il n'y a rien de plus compliqué dans cette partie non plus.
AActor* AExedrePortal::GetTarget() { return Target; } void AExedrePortal::SetTarget( AActor* NewTarget ) { Target = NewTarget; }
Avec cette fonction, nous pouvons facilement vérifier si un point est devant un avion, et dans notre cas c'est un portail. La fonction utilise la structure FPlane du moteur UE4 pour effectuer des calculs.
bool AExedrePortal::IsPointInFrontOfPortal( FVector Point, FVector PortalLocation, FVector PortalNormal ) { FPlane PortalPlane = FPlane( PortalLocation, PortalNormal ); float PortalDot = PortalPlane.PlaneDot( Point );
Cette fonction vérifie si le point a traversé le plan portail. C'est ici que nous utilisons l'ancienne position pour découvrir comment se comporte le point. Cette fonction est courante pour pouvoir fonctionner avec n'importe quel acteur, mais dans mon cas, elle n'est utilisée qu'avec le lecteur.
La fonction crée une direction / segment entre l'emplacement précédent et l'emplacement actuel, puis vérifie s'ils coupent le plan. Si c'est le cas, nous vérifions s'il croise dans la bonne direction (d'avant en arrière?).
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 );
Acteur de téléportation
La dernière partie de l'acteur du portail que nous examinerons est la fonction
TeleportActor () .
Lorsque vous téléportez un acteur du point A au point B, vous devez reproduire son mouvement et sa position. Par exemple, si un joueur passe dans le portail, alors en combinaison avec des effets visuels appropriés, il lui semblera qu'il est passé par une porte ordinaire.
L'intersection du portail donne l'impression de se déplacer en ligne droite, mais en réalité, quelque chose de complètement différent se produit. En quittant le portail, le joueur peut se trouver dans un contexte très différent. Prenons un exemple de Portal:
Comme vous pouvez le voir, lors de la traversée du portail, la caméra pivote par rapport à son vecteur avant (tourne). En effet, les points de départ et d'arrivée sont parallèles à différents plans:
Par conséquent, pour que cela fonctionne, nous devons transformer le mouvement du joueur dans l'espace relatif du portail afin de le convertir en espace cible. En implémentant cela, nous pouvons être sûrs qu'après être entré dans le portail et sortir de l'autre côté, le joueur sera correctement aligné par rapport à l'espace. Cela vaut non seulement pour la position et la rotation de l'acteur, mais aussi pour sa
vitesse .
Si nous téléportons un acteur sans changement, le convertissant en une rotation locale, alors en conséquence, l'acteur peut se retrouver à l'envers. Cela peut convenir aux objets, mais ne s'applique pas aux personnages ou au joueur lui-même. Vous devez modifier la position de l'acteur, comme indiqué ci-dessus dans l'exemple de Portal.
void AExedrePortal::TeleportActor( AActor* ActorToTeleport ) { if( ActorToTeleport == nullptr || Target == nullptr ) { return; }
Comme vous l'avez probablement remarqué, pour appeler rotation / position, j'appelle des fonctions externes. Ils sont appelés à partir de la classe d'utilisateurs UTool, qui définit les fonctions statiques qui peuvent être appelées de n'importe où (y compris les plans). Leur code est illustré ci-dessous, vous pouvez les implémenter de la manière qui vous semble la meilleure (il est probablement plus facile de simplement les mettre dans la classe d'acteur 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; }
La transformation est ici effectuée en calculant le produit scalaire des vecteurs pour déterminer différents angles. Le vecteur Direction n'est pas normalisé, c'est-à-dire que nous pouvons à nouveau multiplier le résultat Dots par des vecteurs cible pour obtenir la position exactement à la même distance dans l'espace local de l'acteur cible.
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(); }
Transformer la transformation était un peu plus difficile à mettre en œuvre. En fin de compte, la meilleure solution s'est avérée être l'utilisation de
quaternions , car cela est beaucoup plus précis que de travailler avec des
angles d'Euler normaux et ne nécessite que quelques lignes de code. Les rotations par quaternions sont effectuées en utilisant la multiplication, donc dans notre cas, en appliquant Inverse () à la rotation que nous voulons convertir, nous la déplacerons vers l'espace local. Ensuite, il suffit de le multiplier à nouveau par le tour cible pour obtenir le tour final.
Création d'un maillage de portail
Pour être belle du point de vue d'un joueur, mon système de portail utilise un maillage spécifique. Le maillage est divisé en deux plans différents:
- Plan 1 : Le plan principal sur lequel la cible de rendu du portail est affichée. Cet avion a un comportement assez inhabituel, car sa tâche est de repousser un peu le joueur alors qu'il s'approche pour éviter l'écrêtage par la caméra. Comme les frontières de l'avion ne bougent pas, mais seulement ses pics centraux se déplacent, cela permet au joueur de se superposer au rendu du portail sans artefacts visuels. Les bords sur les bords ont leur propre UV dans la moitié supérieure, tandis que les bords intérieurs ont leur propre UV dans la moitié inférieure, ce qui facilite leur masquage dans le shader.
- Plan 2 : ce plan est uniquement utilisé pour étendre le cadre de délimitation standard du maillage. Les normales des sommets sont dirigées vers le bas, donc même sur un sol non plan, le maillage ne sera pas visible par défaut (car le matériau de rendu ne sera pas à deux faces).
Pourquoi utiliser un maillage comme celui-ci?
J'ai décidé que «l'avion 1» s'étirerait à l'approche du joueur. Cela permet au joueur de chevaucher le portail et de le traverser sans couper (couper). Cela peut se produire, par exemple, si la caméra n'a pas encore traversé le plan du portail, mais que les pieds du joueur l'ont déjà touché. Cela vous permet de ne pas couper le lecteur et de dupliquer le maillage d'autre part.
La tâche «plan 2» consiste à étendre le cadre de délimitation standard du maillage. Étant donné que le «plan 1» est plat, le cadre de sélection sur un axe a une épaisseur de 0, et si la caméra est derrière, le moteur le coupera (c'est-à-dire qu'il ne le rendra pas). Le plan 1 a une taille de 128 × 128, il peut donc être facilement mis à l'échelle à l'aide du moteur. Le plan 2 est légèrement plus grand et en dessous du sol (en dessous de 0).
Après avoir créé le maillage, nous l'exportons simplement à partir d'un éditeur 3D tiers et l'importons dans Unreal. Il sera utilisé à l'étape suivante.
Création de matériel de portail
Pour afficher l'autre côté du portail, nous devons créer notre propre matériel. Créez du nouveau matériel dans le navigateur de contenu (je l'ai appelé
MAT_PortalBase ):
Ouvrez-le maintenant et créez le graphique suivant:
Voici comment fonctionne le matériau:
- FadeColor est la couleur qui sera visible à travers le portail lorsqu'il est très éloigné. Il est nécessaire car nous ne rendons pas toujours tous les portails, nous masquons donc le rendu lorsque le lecteur / la caméra est loin.
- Pour savoir à quelle distance le joueur est du portail, je détermine la distance entre la position de la caméra et la position de l'acteur. Ensuite, je divise la distance par la valeur maximale avec laquelle je veux effectuer une comparaison. Par exemple, si le maximum que je fixe est de 2000 et que la distance au joueur est de 1000, alors nous obtenons 0,5. Si le joueur est plus loin, alors j'obtiendrai une valeur supérieure à 1, donc j'utilise des nœuds saturés pour le limiter. Vient ensuite le nœud Smoothstep, utilisé pour mettre à l'échelle la distance en tant que gradient et contrôler plus précisément l'ombrage du portail. Par exemple, je veux que lorsque le joueur est proche, l'ombre disparaît complètement.
- J'utilise le calcul de la distance comme valeur de canal alpha pour le nœud Lerp pour mélanger la couleur d'ombrage et la texture qui rendra la cible du portail.
- Enfin, j'isole la composante Y des coordonnées UV pour créer un masque qui vous permet de savoir quels sommets du maillage seront poussés. Je multiplie ce masque par la quantité de répulsion dont j'ai besoin. J'utilise une valeur négative pour que lorsque les normales des sommets sont multipliées par les sommets, elles se déplacent dans la direction opposée.
Après avoir fait tout cela, nous avons créé du matériel prêt à l'emploi.
Création d'un acteur de portail dans Blueprint
Configurons une nouvelle classe de plan héritée de l'acteur Portal. Cliquez avec le bouton droit sur le navigateur de contenu et sélectionnez la classe Blueprint:
Entrez maintenant «portail» dans le champ de recherche pour sélectionner la classe de portail:
Ouvrez Bluetooth s'il n'est pas déjà ouvert. Dans la liste des composants, vous verrez la hiérarchie suivante:
Comme nous nous y attendions, il existe un composant racine et une racine de portail. Ajoutons un composant de maillage statique à PortalRootComponent et chargeons-y le maillage créé à l'étape précédente:
Nous ajoutons également la Collision Box, qui sera utilisée pour déterminer si le joueur est à l'intérieur du volume du portail:
La zone Collision est située sous le composant de scène associé à la racine principale et non sous la racine du portail. J'ai également ajouté une icône (panneau d'affichage) et un composant flèche pour rendre le portail plus visible aux niveaux. Bien sûr, ce n'est pas nécessaire.
Maintenant, configurons le matériau dans le plan.
Pour commencer, nous avons besoin de deux variables - l'une sera de type
Actor et le nom est
PortalTarget , la seconde est de type
Dynamic Material Instance et s'appelle
MaterialInstance . PortalTarget sera une référence à la position que la fenêtre du portail regarde (par conséquent, la variable est commune, avec une icône en forme d'œil ouvert) afin que nous puissions la changer lorsque l'acteur est placé au niveau. MaterialInstance stockera un lien vers du matériel dynamique afin qu'à l'avenir nous puissions assigner la cible de rendu du portail à la volée.
Nous devons également ajouter nos propres nœuds d'événements. Il est préférable d'ouvrir le menu contextuel dans le
graphique des événements et de trouver les noms des événements:
Et ici pour créer le diagramme suivant:
- Commencer la lecture : ici, nous appelons la fonction parent SetTarget () du portail pour lui attribuer un lien vers l'acteur, qui sera ensuite utilisé pour SceneCapture. Nous créons ensuite un nouveau matériau dynamique et lui attribuons la valeur de la variable MaterialInstance. Avec ce nouveau matériau, nous pouvons l'assigner au composant de maillage statique. J'ai également donné au matériau une texture factice, mais cela est facultatif.
- Clear RTT : Le but de cette fonction est d'effacer la texture Render Target affectée au matériau du portail. Il est lancé par le gestionnaire de portail.
- Définir RTT : le but de cette fonction est de définir le matériau cible de rendu du portail. Il est lancé par le gestionnaire de portail.
Jusqu'à présent, nous avons terminé avec le bluetooth, mais nous y reviendrons plus tard pour implémenter les fonctions Tick.
Gestionnaire de portail
Donc, nous avons maintenant tous les éléments de base nécessaires pour créer une nouvelle classe héritée d'AActor, qui sera Portal Manager. Vous n'avez peut-être pas besoin de la classe Portal Manager dans votre projet, mais dans mon cas, cela simplifie considérablement le travail avec certains aspects. Voici une liste des tâches effectuées par le gestionnaire de portail:
- Le gestionnaire de portail est un acteur créé par le contrôleur de joueur et attaché à lui pour suivre l'état et l'évolution du joueur dans le niveau de jeu.
- Créez et détruisez le portail cible de rendu . L'idée est de créer dynamiquement une texture cible de rendu qui correspond à la résolution d'écran du lecteur. De plus, lors du changement de résolution pendant le jeu, le manager la convertira automatiquement à la taille souhaitée.
- Le gestionnaire de portail recherche et met à jour le niveau d'acteur de portail pour leur donner une cible de rendu. Cette tâche est effectuée de manière à assurer la compatibilité avec le streaming de niveau. Lorsqu'un nouvel acteur apparaît, il devrait obtenir une texture. De plus, si la cible de rendu change, le gestionnaire peut également en attribuer une nouvelle automatiquement. Cela facilite la gestion du système, au lieu que chaque acteur du portail contacte manuellement le gestionnaire.
- Le composant SceneCapture est attaché au gestionnaire de portail, afin de ne pas créer une copie pour chaque portail. De plus, il vous permet de le réutiliser chaque fois que nous passons à un acteur de portail spécifique au niveau.
- Lorsque le portail décide de téléporter le joueur, il envoie une demande au gestionnaire de portail. Cela est nécessaire pour mettre à jour les portails source et de destination (le cas échéant), afin que la transition se fasse sans joints.
- Le gestionnaire de portail est mis à jour à la fin de la fonction tick () du personnage afin que tout soit correctement mis à jour, y compris la caméra du joueur. Cela garantit que tout sur l'écran est synchronisé et évite un retard d'une image lors du rendu par le moteur.
Jetons un coup d'œil à l'en-tête de Portal Manager:
#pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ExedrePortalManager.generated.h"
Avant d'entrer dans les détails, je vais montrer comment un acteur est créé à partir de la classe Player Controller, appelée à partir de la fonction 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();
Ainsi, nous créons un acteur, l'attachons au contrôleur du joueur (ceci), puis enregistrons le lien et appelons la fonction Init ().
Il est également important de noter que nous mettons à jour l'acteur manuellement à partir de la classe Character:
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 ); } }
Et voici le constructeur de Portal Manager. Notez que Tick est désactivé, car nous mettrons à jour manuellement Portal Manager via le lecteur.
AExedrePortalManager::AExedrePortalManager(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { PrimaryActorTick.bCanEverTick = false; PortalTexture = nullptr; UpdateDelay = 1.1f; PreviousScreenSizeX = 0; PreviousScreenSizeY = 0; }
Voici les fonctions de get / set Portal Manager (après quoi nous passerons à des choses plus intéressantes):
void AExedrePortalManager::SetControllerOwner( AExedrePlayerController* NewOwner ) { ControllerOwner = NewOwner; } FTransform AExedrePortalManager::GetCameraTransform() { if( SceneCapture != nullptr ) { return SceneCapture->GetComponentTransform(); } else { return FTransform(); } } UTexture* AExedrePortalManager::GetPortalTexture() {
De toute évidence, la première chose à commencer est la fonction
Init () .
L'objectif principal de cette fonction est de créer le composant SceneCapture (c'est-à-dire le périphérique de capture mentionné ci-dessus) et de le configurer correctement. Cela commence par la création d'un nouvel objet et son enregistrement en tant que composante de cet acteur. Ensuite, nous passons à la définition des propriétés liées à cette capture.Propriétés à mentionner:- bCaptureEveryFrame = false : nous ne voulons pas que la capture soit activée lorsque nous n'en avons pas besoin. Nous le gérerons manuellement.
- bEnableClipPlane = true : propriété assez importante pour rendre correctement la capture du portail.
- bUseCustomProjectionMatrix = true : cela nous permet de remplacer la projection Capture par la nôtre, en fonction du point de vue du joueur.
- CaptureSource = ESceneCaptureSource :: SCS_SceneColorSceneDepth : Ce mode est un peu cher, mais il est nécessaire de restituer une quantité d'informations suffisante.
Les propriétés restantes sont principalement liées aux paramètres de post-traitement. Ils sont un moyen pratique de contrôler la qualité et donc de capturer les performances.La dernière partie appelle la fonction qui crée la cible de rendu, que nous verrons ci-dessous. void AExedrePortalManager::Init() {
GeneratePortalTexture () est une fonction qui est appelée lorsque cela est nécessaire lorsque vous devez créer une nouvelle texture cible de rendu pour les portails. Cela se produit dans la fonction d'initialisation, mais il peut également être appelé lors de la mise à niveau de Portal Manager. C'est pourquoi cette fonction dispose d'un contrôle interne pour modifier la résolution de la fenêtre. Si cela ne s'est pas produit, la mise à jour n'est pas effectuée.Dans mon cas, j'ai créé une classe wrapper pour UCanvasRenderTarget2D. Je l'ai appelé ExedreScriptedTexture, c'est un composant qui peut être attaché à un acteur. J'ai créé cette classe pour gérer facilement les cibles de rendu avec des acteurs qui ont des tâches de rendu. Il effectue l'initialisation appropriée de la cible de rendu et est compatible avec mon propre système d'interface utilisateur. Cependant, dans le contexte des portails, une texture RenderTarget2D régulière est plus que suffisante. Par conséquent, vous pouvez simplement l'utiliser. 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);
Comme mentionné ci-dessus, j'ai créé ma propre classe, donc les propriétés définies ici doivent être adaptées à la cible de rendu habituelle.Il est important de comprendre où la capture sera affichée. Étant donné que la cible de rendu sera affichée dans le jeu, cela signifie que cela se produira avant tout le post-traitement, et nous devons donc rendre la scène avec suffisamment d'informations (pour stocker des valeurs supérieures à 1 pour créer Bloom). C'est pourquoi j'ai choisi le format RGBA16 (notez qu'il a son propre Enum, vous devrez utiliser ETextureRenderTargetFormat à la place).Pour plus d'informations, consultez les sources suivantes:
De plus, nous considérerons les fonctions de mise à jour. La fonction de base est assez simple et rend plus complexe. Il existe un délai avant les appels à la fonction GeneratePortalTexture () pour éviter de recréer la cible de rendu lors du redimensionnement de la fenêtre (par exemple, dans l'éditeur). Lors de la publication du jeu, ce délai peut être supprimé. void AExedrePortalManager::Update( float DeltaTime ) {
Nous appelons UpdatePortalsInWorld () pour trouver tous les portails présents dans le monde actuel (y compris tous les niveaux chargés) et les mettre à jour. La fonction détermine également lequel est «actif», c'est-à-dire visible pour le joueur. Si nous trouvons un portail actif, nous appelons UpdateCapture () , qui contrôle le composant SceneCapture.
Voici comment fonctionne la mise à jour mondiale dans UpdatePortalsInWorld () :- ( )
- iterator ,
- , , ClearRTT() , . (, ).
- , , , , .
Le contrôle qui détermine l'exactitude du portail est simple: nous donnons la priorité au portail le plus proche du joueur, car il sera très probablement le plus visible de son point de vue. Pour supprimer des proches, mais, par exemple, des portails situés derrière le joueur, des vérifications plus complexes seront nécessaires, mais je ne voulais pas me concentrer sur cela dans mon tutoriel, car cela peut devenir assez difficile. AExedrePortal* AExedrePortalManager::UpdatePortalsInWorld() { if( ControllerOwner == nullptr ) { return nullptr; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
Il est temps de considérer la fonction UpdateCapture () .Il s'agit d'une fonctionnalité de mise à niveau qui capture l'autre côté du portail. D'après les commentaires, tout doit être clair, mais voici une brève description:- Nous obtenons des liens vers le contrôleur de personnage et de joueur.
- Nous vérifions si tout est correct (Portal, composant SceneCapture, Player).
- Camera Target .
- , SceneCapture.
- SceneCapture Target.
- , SceneCapure , , .
- Render Target SceneCapture, .
- PlayerController.
- , Capture SceneCapture .
Comme nous pouvons le voir, lors de la téléportation d'un joueur, un élément clé du comportement naturel et sans défaut de SceneCapture est la transformation correcte de la position et de la rotation du portail dans l'espace cible local.Pour la définition de ConvertLocationToActorSpace (), voir «Téléportation d'un acteur».
void AExedrePortalManager::UpdateCapture( AExedrePortal* Portal ) { if( ControllerOwner == nullptr ) { return; } AExedreCharacter* Character = ControllerOwner->GetCharacter();
La fonction GetCameraProjectionMatrix () n'existe pas par défaut dans la classe PlayerController, je l'ai ajoutée moi-même. Il est illustré ci-dessous: FMatrix AExedrePlayerController::GetCameraProjectionMatrix() { FMatrix ProjectionMatrix; if( GetLocalPlayer() != nullptr ) { FSceneViewProjectionData PlayerProjectionData; GetLocalPlayer()->GetProjectionData( GetLocalPlayer()->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, PlayerProjectionData ); ProjectionMatrix = PlayerProjectionData.ProjectionMatrix; } return ProjectionMatrix; }
Enfin, nous devons implémenter l'appel à la fonction Teleport. La raison du traitement partiel de la téléportation via le gestionnaire de portail est qu'il est nécessaire de garantir la mise à jour des portails nécessaires, car seul le gestionnaire dispose d'informations sur tous les portails de la scène.Si nous avons deux portails connectés, lors du passage de l'un à l'autre, nous devons mettre à jour les deux en une seule fois. Sinon, le joueur se téléportera et sera de l'autre côté du portail, mais le portail cible ne sera pas actif avant la prochaine image / mesure. Cela créera des espaces visuels avec le matériau décalé du maillage plan que nous avons vu ci-dessus. void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { Portal->TeleportActor( TargetToTeleport );
Eh bien, c'est tout, nous avons finalement terminé avec Portal Manager!Terminer le plan
Une fois le gestionnaire de portail terminé, il nous suffit de terminer l'acteur du portail lui-même, après quoi le système fonctionnera. La seule chose qui manque ici est les fonctionnalités de Tick:Voici comment cela fonctionne:- Nous mettons à jour le matériel afin qu'il ne reste pas dans un état actif.
- Si le portail est actuellement inactif , le reste de la mesure est rejeté.
- Nous obtenons la classe Personnage pour accéder à l' emplacement de la caméra .
- La première partie vérifie si la caméra se trouve dans la zone de collision du portail. Si c'est le cas, nous compensons le maillage du portail avec son matériau .
- La deuxième partie consiste à revérifier l'emplacement à l'intérieur de la zone de collision. S'il est exécuté, nous appelons une fonction qui vérifie si nous traversons le portail .
- , Portal manager, Teleport .
Dans la capture d'écran de mon graphique, vous pouvez remarquer deux points intéressants: Is Point Inside Box et Get Portal Manager . Je n'ai pas encore expliqué ces deux fonctions. Ce sont des fonctions statiques que j'ai définies dans ma propre classe afin que vous puissiez les appeler de n'importe où. Il s'agit d'une sorte de classe d'aide. Le code de ces fonctions est indiqué ci-dessous, vous pouvez vous-même décider où les insérer. Si vous n'en avez pas besoin en dehors du système de portail, vous pouvez les insérer directement dans la classe d'acteur de portail.Au début, je voulais utiliser le système de collision pour déterminer si l'acteur de portail à l'intérieur de la zone de collision se trouve dans le portail, mais il ne m'a pas semblé suffisamment fiable. De plus, il me semble que cette méthode est plus rapide à utiliser et présente un avantage: elle prend en compte la rotation de l'acteur. bool IsPointInsideBox( FVector Point, UBoxComponent* Box ) { if( Box != nullptr ) {
AExedrePortalManager* GetPortalManager( AActor* Context ) { AExedrePortalManager* Manager = nullptr;
La dernière partie de l'acteur Blueprint est ForceTick . N'oubliez pas que Force Tick est appelé lorsqu'un joueur traverse un portail et se trouve à côté d'un autre portail pour lequel Portal Manager force une mise à jour. Comme nous venons de nous téléporter, il n'est pas nécessaire d'utiliser le même code, et vous pouvez utiliser sa version simplifiée:Le processus démarre approximativement en même temps que la fonction Tick, mais nous n'exécutons que la première partie de la séquence, qui met à jour le matériel.Avons-nous fini?
Presque.Si nous implémentons le système de portail sous cette forme, nous rencontrerons très probablement le problème suivant:Que se passe-t-il ici?Dans ce gif, la fréquence d'images du jeu est limitée à 6 FPS pour montrer plus clairement le problème. Dans une image, le cube disparaît car le système d'écrêtage d' Unreal Engine le considère comme invisible.En effet, la découverte est effectuée dans la trame actuelle, puis utilisée dans la suivante. Cela crée un retard d'une trame . Cela peut généralement être résolu en développant le cadre de sélection de l'objet afin qu'il soit enregistré avant qu'il ne devienne visible. Cependant, cela ne fonctionnera pas ici, car lorsque nous traversons le portail, nous nous téléportons d'un endroit à un autre.La désactivation du système d'écrêtage est également impossible, en particulier parce qu'aux niveaux avec de nombreux objets, cela réduira les performances. De plus, j'ai essayé de nombreuses équipes du moteur Unreal, mais je n'ai pas obtenu de résultats positifs: dans tous les cas, un retard d'une trame est resté. Heureusement, après une étude détaillée du code source d'Unreal Engine, j'ai réussi à trouver une solution (le chemin était long - il a fallu plus d'une semaine)!Comme avec le composant SceneCapture, vous pouvez dire à la caméra du lecteur que nous avons effectué une coupure de saut- la position de la caméra a sauté entre deux images, ce qui signifie que nous ne pouvons pas nous fier aux informations de l'image précédente. Ce comportement peut être observé lors de l'utilisation de Matinee ou Sequencer, par exemple, lors du changement de caméra: le flou de mouvement ou le lissage ne peut pas s'appuyer sur les informations de l'image précédente.Pour ce faire, nous devons considérer deux aspects:- LocalPlayer : cette classe gère diverses informations (par exemple, la fenêtre du joueur) et est associée au PlayerController. C’est là que nous pouvons influencer le processus de rendu de la caméra du lecteur.
- PlayerController : lorsqu'un joueur se téléporte, cette classe commence à épisser grâce à l'accès à LocalPlayer.
Le gros avantage de cette solution est que l'intervention dans le processus de rendu du moteur est minimale et facile à entretenir dans les futures mises à jour d'Unreal Engine.
Commençons par créer une nouvelle classe héritée de LocalPlayer. Vous trouverez ci-dessous un en-tête qui identifie deux composants principaux: la redéfinition des calculs de la fenêtre de scène et une nouvelle fonction pour invoquer le collage de la caméra. #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; };
Voici comment tout est mis en œuvre: #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) {
PerformCameraCut () démarre simplement Camera Cut avec une valeur booléenne. Lorsque le moteur appelle la fonction CalcSceneView () , nous exécutons d' abord la fonction d'origine. Ensuite, nous vérifions, nous devons effectuer un collage. Si c'est le cas, nous redéfinissons la variable booléenne Camera Cut à l'intérieur de la structure FSceneView , qui sera utilisée par le processus de rendu du moteur, puis réinitialisons la variable booléenne (utilisez-la).
Côté Player Controller, les changements sont minimes. Vous devez ajouter une variable à l'en-tête pour stocker un lien vers la classe native LocalPlayer: UPROPERTY() UExedreLocalPlayer* LocalPlayer;
Puis dans la fonction BeginPlay () : LocalPlayer = Cast<UExedreLocalPlayer>( GetLocalPlayer() );
J'ai également ajouté une fonction pour lancer rapidement Cut: void AExedrePlayerController::PerformCameraCut() { if( LocalPlayer != nullptr ) { LocalPlayer->PerformCameraCut(); } }
Enfin, dans la fonction Portal Manager RequestTeleportByPortal (), nous pouvons exécuter pendant la téléportation Camera Cut: void AExedrePortalManager::RequestTeleportByPortal( AExedrePortal* Portal, AActor* TargetToTeleport ) { if( Portal != nullptr && TargetToTeleport != nullptr ) { if( ControllerOwner != nullptr ) { ControllerOwner->PerformCameraCut(); } [...]
Et c'est tout!Camera Cut doit être appelé avant la mise à jour de SceneCapture, c'est pourquoi il se trouve au début de la fonction.Résultat final
Maintenant, nous avons appris à penser dans les portails.Si le système fonctionne bien, nous devrions pouvoir créer ces choses:Si vous rencontrez des problèmes, vérifiez les points suivants:- Vérifiez que Portal Manager est correctement créé et initialisé.
- La cible de rendu est créée correctement (vous pouvez utiliser celle créée dans le navigateur de contenu pour commencer).
- Les portails sont correctement activés et désactivés.
- Les portails ont l'acteur cible correctement défini dans l'éditeur.
Q & A
Les questions les plus fréquemment posées à propos de ce tutoriel:est-il possible de l'implémenter sur des contours, et non via C ++?La majeure partie du code peut être implémentée en blunts, à l'exception de deux aspects:- La fonction LocalPlayer GetProjectionData () utilisée pour obtenir la matrice de projection n'est pas disponible dans les plans.
- La fonction LocalPlayer CalcSceneView () , qui est essentielle pour résoudre le problème du système d'écrêtage, n'est pas disponible dans les plans.
Par conséquent, vous devez soit utiliser une implémentation C ++ pour accéder à ces deux fonctions, soit modifier le code source du moteur pour les rendre accessibles via des plans directeurs.Puis-je utiliser ce système en VR?Oui, pour la plupart. Cependant, certaines parties devront être adaptées, par exemple:- Vous devez utiliser deux cibles de rendu (une pour chaque œil) et les masquer dans le matériau du portail pour les afficher côte à côte dans l'espace d'écran. Chaque cible de rendu doit avoir la moitié de la largeur de la résolution de l'appareil VR.
- Vous devez utiliser deux SceneCapture pour rendre la cible avec la bonne distance (la distance entre les yeux) pour créer des effets stéréoscopiques.
Le problème principal sera la performance, car l'autre côté du portail devra être rendu deux fois.Un autre objet peut-il traverser le portail?Il n'y en a pas dans mon code. Cependant, le rendre plus général n'est pas si difficile. Pour ce faire, le portail doit suivre plus d'informations sur tous les objets à proximité afin de vérifier s'ils le traversent.Le système prend-il en charge la récursivité (portail à l'intérieur du portail)?Ce tutoriel ne l'est pas. Pour la récursivité, vous avez besoin d'une cible de rendu supplémentaire et de SceneCapture. Il sera également nécessaire de déterminer quel RenderTarget rendre en premier, et ainsi de suite. C'est assez difficile et je ne voulais pas le faire, car pour mon projet ce n'est pas nécessaire.Puis-je traverser le portail près du mur?Malheureusement non. Cependant, je vois deux façons de mettre en œuvre cela (théoriquement):- Désactivez les collisions du joueur afin qu'il puisse passer à travers les murs. Il est facile à mettre en œuvre, mais il entraînera de nombreux effets secondaires.
- Piratez un système de collision pour créer un trou dynamiquement, ce qui permettra au joueur de passer. Pour ce faire, vous devez modifier le système physique du moteur. Cependant, d'après ce que je sais, après avoir chargé le niveau, la physique statique ne peut pas être mise à jour. Par conséquent, pour prendre en charge cette fonctionnalité, il faudra beaucoup de travail. Si vos portails sont statiques, vous pouvez probablement contourner ce problème en utilisant le streaming de niveau pour basculer entre les différentes collisions.