FMX Island épisode 1 : génération de mondes extérieurs en 3D avec Delphi et Firemonkey

Grégory Bersegeay

Toujours dans la découverte de la 3D avec Firemonkey, nous allons nous intéresser à la construction d’une scène en 3D en extérieur. Pour ce faire, nous allons aborder plusieurs notions au cours d’une série de tutoriels. Ce premier épisode va nous permettre de générer des paysages en 3D en utilisant la technique du champ de hauteur (ou heightmap).

Commentez

- Titre : FMX Island épisode 1 : génération de mondes extérieurs en 3D avec Delphi et Firemonkey

- Auteur : Grégory Bersegeay

- Parution : 27 mai 2018

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2018 Grégory Bersegeay. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Pré requis

- II. Présentation du projet

- III. Génération d’un monde 3D

- III-A. Technique du HeightMap (théorie)

- III-B. Implémentation sous Delphi

- III-C. Quelques améliorations

- IV. Conclusion de cet épisode et la suite

- V. Remerciements

I - Pré requis

Comme pour mes précédents tutoriels, celui-ci a été réalisé sous Delphi (version Tokyo 10.2.3). Il utilise uniquement les composants standards. Le projet est compatible avec la version précédente Berlin et je n’ai pas fait d’essai avec des versions antérieures.

D’un point de vue développement, si vous connaissez déjà Delphi, c’est un plus, car je ne détaillerai pas comment ouvrir un projet, compiler, naviguer dans la palette des composants et l’inspecteur d’objets… Si vous ne connaissez pas Delphi, il n’y a rien de compliqué, mais ces actions ne seront pas détaillées dans ce tutoriel.

Avant de commencer et comme nous allons travailler en 3D, voici l’orientation du repère orthonormé par défaut lorsqu’on travaille en 3D avec Firemonkey :

Bien avoir ce repère en tête permet de mieux s’orienter par la suite lorsqu’on veut placer un objet, le faire tourner, le déplacer…

De plus, comme pour mes précédents tutoriels, celui-ci utilise une source lumineuse. Pour gérer les lumières, Firemonkey utilise les shaders (sous Windows c’est le shader model 5 sous Direct X 11). Si votre carte graphique ne les gère pas, vous devrez forcer le rendu logiciel via la ligne suivante dans le source du projet avant l’instruction Application.Initialize; :

fmx.types.GlobalUseDXSoftware := True;

Enfin, nous allons utiliser des images. Je fournis les textures utilisées dans ce tutoriel et il vous faudra les placer dans un sous-répertoire nommé textures du répertoire où vous générez l’exécutable.

II - Présentation du projet

Mes précédents tutoriels étaient des reprises de jeux vidéo en 3D. Cette fois, il ne s’agit ni d’une reprise à proprement parler ni d’un jeu complet, mais du résultat de différentes explorations que j’ai faites pour créer un monde ouvert en 3D.

La série de tutoriels que je vous propose sera composée de plusieurs épisodes courts, progressifs et accessibles à tout le monde. Afin d’illustrer les notions que nous allons y voir, nous construirons ensemble un projet qui servira de fil rouge. À la fin de la série, vous obtiendrez le projet FMX Island dont voici une capture d’écran :

Toutefois, le projet FMX Island n’est pas une fin en soi : il s’agit d’un exemple. Une fois les principes de base acquis, votre curiosité et votre créativité pourront (devront ?) prendre le relais ! Si ces tutoriels vous inspirent, je serai ravi de voir vos réalisations !

Au programme de la série :

- épisode 1 : génération de paysages via la technique du champ de hauteur (heightmap) ;

- épisode 2 : déplacements et orientation ;

- épisode 3 : diverses améliorations (cycle jour/nuit, carte d’orientation, mer animée, nuages…) ;

- épisode 4 : détection des collisions.

Les épisodes s’enchaînent : il est fortement conseillé de les suivre dans l’ordre.

Voici pour la présentation du projet : passons maintenant aux choses sérieuses.

III - Génération d’un monde 3D

Le titre du paragraphe est un peu exagéré : nous allons générer une île virtuelle en 3D ! Le principe reste le même pour créer un monde plus grand. Bien sûr, plus le monde sera grand, plus il faudra un ordinateur puissant, car le nombre de polygones à gérer sera important.

Comme indiqué, nous allons utiliser la technique des champs de hauteur (HeightMap) pour générer l’île.

III-A - Technique du HeightMap (théorie)

Cette technique est largement utilisée en infographie pour générer un monde tridimensionnel à partir d’une simple image en deux dimensions.

Le principe est simple. Prenons une image en noir et blanc telle que celle utilisée dans ce tutoriel :

La couleur de chaque pixel va correspondre à une hauteur : un pixel noir sera au plus bas, un pixel blanc sera au plus haut. Évidemment, les couleurs intermédiaires seront à des hauteurs comprises entre ces deux extrêmes, un peu comme sur une carte classique avec les courbes de niveau.

Il est possible d’utiliser des images en couleurs pour obtenir un rendu encore plus précis : en noir et blanc, nous n’avons « que » 256 niveaux de hauteur possibles, mais cela est largement suffisant pour ce tutoriel.

Il existe des logiciels permettant de générer de telles images (Terragen, Picogen…) et les moteurs 3D de jeux vidéo implémentent cette technique pour gérer les terrains.

Pour en savoir plus sur le HeightMap, vous pouvez consulter la page Wikipédia correspondante.

III-B - Implémentation sous Delphi

Le passage de la théorie à la pratique peut parfois être difficile. Pas d’inquiétude, nous allons le faire en douceur !

Ouvrez Delphi, créez un nouveau projet de type « Application multi-périphérique » puis sélectionnez le modèle « Application vide ».

Vous devez obtenir ceci :

Commençons par renommer la fiche via l’inspecteur d’objets en modifiant sa propriété Name à fPrincipale. Renseignons également sa propriété Caption à Episode 1.

Enregistrez ensuite le projet sous le répertoire de votre choix en nommant le projet episode1, et l’unit1 sous le nom principale.pas.

Nous allons placer tous les composants dont nous allons avoir besoin sur notre fiche. Dans la palette d’outils, recherchez le composant TViewport3D (soit en saisissant son nom dans la zone de recherche, soit en allant dans la rubrique « Fenêtres d’affichage »).

Sélectionnez le composant et placez-le sur la fiche.

Dans l’inspecteur d’objets, affectez sa propriété align à « client » afin que le TViewPort3D occupe toute la fiche.

Sélectionnez ensuite sa propriété color afin de lui affecter une autre couleur que le blanc par défaut (par exemple la couleur SteelBlue). Toujours grâce à l’inspecteur d’objets, renommez le TViewport3D en viewport.

Je vous conseille d’utiliser systématiquement un composant TDummy (rubrique « Scène 3D ») qui n’est pas visible à l’exécution, mais qui permet de regrouper d’autres composants 3D afin de pouvoir leur appliquer des transformations (déplacements, étirements, rotations, transformations…) d’un seul coup.

Placez donc un composant TDummy dans le TViewport3D et nommez-le dmyMonde. Il contiendra les principaux éléments de notre scène 3D.

Pour le rendu de notre île, nous allons utiliser un composant TMesh (rubrique « Formes 3D »).

Ce composant permet de gérer un objet 3D complexe, c’est-à-dire un ensemble de polygones (des triangles) liés les uns aux autres (dans un maillage ou mesh en anglais). C’est la propriété Data de l’objet TMesh qui contient toutes les informations (en particulier les coordonnées de tous les sommets des polygones composant l’objet 3D à afficher).

Placez un composant TMesh sur la fiche et via la vue Structure, placez-le en tant qu’enfant de dmyMonde. Via l’inspecteur d’objets, renommez le TMesh en mSol.

Enfin, placez un composant TColorMaterialSource (rubrique « Matériaux ») sur la fiche. Ce composant permet de sélectionner une simple couleur : il servira un peu plus tard pour tracer les arêtes des polygones du TMesh.

Via l’inspecteur d’objets, modifiez sa propriété Color pour la mettre à Blue par exemple et sa propriété Name à couleurMaillage.

Nous avons préparé l’interface, il va maintenant falloir écrire quelques lignes de code.

Commençons par ajouter quelques unités dont nous allons avoir besoin à la clause uses : FMX.types3D, FMX.Effects, System.UIConsts.

Dans la déclaration de la classe TfPrincipale, ajoutons une variable nommée maHeightMap de type TBitmap dans la section public. Cette variable contiendra notre image heightmap.

maHeightMap: TBitmap;    // Texture qui servira à générer le sol (le Mesh)

Ensuite, après la déclaration de TfPrincipale, ajoutons la déclaration du type suivant :

TMeshHelper = class(TCustomMesh); // Va servir pour caster un TPlane en TMesh

Ce type nous sera utile par la suite pour « convertir » un TPlane (un plan) en TMesh. En effet, dans ce tutoriel, le TMesh sera créé à partir d’un TPlane carré. Pourquoi carré ? Car l’image de heightmap que j’ai choisie est carrée.

Ajoutons les deux constantes suivantes :

const
MaxSolMesh = 511;  // Nombre de mailles sur un côté du TMesh
SizeMap = 512;  // Taille du côté du TMesh

La constante MaxSolMesh va correspondre au nombre de subdivisions que nous ferons d’un TPlane. Ces subdivisions vont permettre de faire un quadrillage (maillage) du TPlane. Plus ce maillage sera important, plus le rendu final du relief sera fin, car il y aura plus de polygones pour modéliser l’île. La contrepartie sera la nécessité de disposer d’un ordinateur équipé d’une carte graphique puissante.

Attention : MaxSolMesh ne peut prendre une valeur supérieure à la taille en pixel d’un côté de notre image heightmap moins 1. En effet, comme chaque pixel de la heightmap va nous servir à générer le relief (positionner le polygone de manière adéquate), il ne peut y avoir plus de polygones que de pixels. D’où la valeur 511 car notre heightmap fait 512 pixels de côté.

La constante SizeMap peut prendre n’importe quelle valeur.

Dans la vue Structure, sélectionnez la fiche fPrincipale, puis dans l’inspecteur d’objets, onglet événements, générez l’événement OnCreate.

Tout d’abord, dans le gestionnaire créé, écrivons les lignes suivantes :

procedure TfPrincipale.FormCreate(Sender: TObject);
begin
ChargerTextures; // Charge les différentes textures
CreerIle(MaxSolMesh);  // Création de l’île
end;

Il s’agit de faire appel à deux procédures que nous allons définir maintenant.

La procédure ChargerTextures va permettre d’initialiser la variable maHeightMap en créant l’objet et en chargeant l’image.

procedure TfPrincipale.ChargerTextures; // Chargement des textures
begin
maHeightMap:=TBitmap.Create;
maHeightMap.LoadFromFile('.'+PathDelim+'textures'+PathDelim+'heightmap.jpg');
end;

La procédure CreerIle va permettre, comme son nom l’indique, de créer l’île. Nous allons donc travailler l’objet TMesh mSol et le « sculpter » via sa propriété Data.

Pour le moment, la propriété Data est vide et la renseigner directement par le code serait fastidieux. Nous allons nous servir d’un TPlane qui va servir de base. Ce composant dispose des propriétés SubdivisionsHeight et SubdivisionsWidth permettant de le découper en zones. Le nombre de zones sera le nombre passé en paramètre à la procédure CreerIle (pour rappel : notre heightmap est carrée, donc SubdivisionsHeight et SubdivisionsWidth seront de valeur identique).

Nous créons ensuite un objet de type TMeshData et nous lui affectons le maillage du TPlane créé précédemment. Pour ce faire, nous passons par un TMeshHelper qui permet de convertir le TPlane en TMesh afin d’accéder à sa propriété Data.

À cet instant, notre objet TMeshData contient les informations nécessaires pour créer un objet 3D plat. En affectant ce TMeshData au TMesh mSol présent sur notre fiche, nous obtiendrions ni plus ni moins qu’un TPlane.

Il faut maintenant lui donner du volume.

Nous allons flouter légèrement l’image afin d’adoucir les angles et ainsi générer des montagnes moins anguleuses (si un point noir est juste à côté d’un point blanc, on aura une maille verticale dans le résultat final : cela est peu naturel). Appelons tout simplement la méthode Blur présente dans FMX.Effects.

Ensuite, pour accéder directement aux données du TBitmap maHeightMap, il faut passer par sa méthode map. Nous pourrons ainsi accéder aux pixels de l’image et récupérer la couleur de chaque pixel.

Nous parcourons ensuite toutes les mailles que nous avons créées en subdivisant le TPlane.

Dans mon exemple, j’ai subdivisé en 511x511 : cela donnera donc 261 121 polygones. Nous récupérons ainsi chaque maille, ses coordonnées X et Y, puis nous lisons la couleur du pixel de la heightmap qui se trouve à ces coordonnées.

Pourquoi 511x511 ? Tout simplement par ce que notre heightmap fait 512x512 pixels. Chaque pixel représentera un sommet de maille : la première maille sera entre le premier et le deuxième pixel, la seconde maille sera entre le deuxième pixel et le troisième, etc. De même pour chaque ligne : on obtient donc bien 511x511 mailles pour une heightmap de 512x512 pixels.

Nous récupérons la couleur du pixel puis nous en calculons une hauteur comme nous le souhaitons. Dans l’exemple fourni, et comme indiqué précédemment, la heightmap est une image en niveau de gris. Il y a donc 256 couleurs, ce qui signifie que nous pourrons générer jusqu’à 256 « niveaux d’altitude ». C’est suffisant pour cet exemple, mais, si vous avez besoin de plus de précision, vous pouvez utiliser une heightmap en couleurs. À vous alors de générer le relief comme vous l’entendez en fonction des niveaux des couleurs sur les canaux RVB.

Nous affectons la valeur trouvée à la coordonnée Z de la maille que nous sommes en train de traiter.

Une fois toutes les mailles traitées, nous invoquons la méthode CalcTangentBinormals du TMeshData afin de mettre à jour toutes les autres informations contenues dans cet objet. En effet, nous nous sommes juste intéressés à la position des points constituant notre objet, mais le TMeshData contient d’autres informations (vecteurs binormaux et de tangente). Cela est nécessaire par exemple pour ensuite bien réagir à la lumière.

Enfin, nous arrivons à notre objet TMesh qui lui sera l’objet affiché. Nous le taillons, par exemple, avec une largeur et une profondeur de SizeMap et une hauteur de 50 (=> valeur arbitraire). Puis nous affectons à sa propriété Data le TMeshData que nous venons de calculer.

Nous allons coder cela et voici le code de la procédure CreerIle :

procedure TfPrincipale.CreerIle(const nbSubdivisions: integer); // Création du niveau
var
Basic : TPlane;             // TPlane qui va servir de base
SubMap : TBitMap;           // Bitmap qui va servir pour générer le relief à partir du heightmap
Front, Back : PPoint3D;
M: TMeshData;               // informations du Mesh
G, S, W, X, Y: Integer;
zMap : Single;
C : TAlphaColorRec;         // Couleur lue dans la heightmap et qui sert à déterminer la hauteur d'un sommet
bitmapData: TBitmapData;    // nécessaire pour pouvoir accéder aux pixels d'un TBitmap
begin
if nbSubdivisions < 1 then exit;  // il faut au moins une subdivision

G:=nbSubdivisions + 1;
S:= G * G;  // Nombre total de mailles (la heightmap est carrée)

try
Basic := TPlane.Create(nil);    // Création du TPlane qui va servir de base à la constitution du mesh
Basic.SubdivisionsHeight := nbSubdivisions; // le TPlane sera carré et subdivisé pour le maillage (mesh)
Basic.SubdivisionsWidth := nbSubdivisions;

M:=TMeshData.create;       // Création du TMesh
M.Assign(TMEshHelper(Basic).Data); // les données sont transférées du TPlane au TMesh

SubMap:=TBitmap.Create(maHeightMap.Width,maHeightMap.Height);  // Création du bitmap
SubMap.Assign(maHeightMap);    // On charge la heightmap

blur(SubMap.canvas, SubMap, 8); // On floute l'image afin d'avoir des montagnes moins anguleuses

if (SubMap.Map(TMapAccess.Read, bitmapData)) then  // nécessaire pour accéder au pixel du Bitmap afin d'en récupérer la couleur
begin
try
for W := 0 to S-1 do  // Parcours de tous les sommets du maillage
begin
Front := M.VertexBuffer.VerticesPtr[W];    // Récupération des coordonnées du sommet (TPlane subdivisé pour rappel : on a les coordonnées en X et Y et Z est encore à 0 pour l'instant)
Back := M.VertexBuffer.VerticesPtr[W+S];   // Pareil pour la face arrière
X := W mod G; // abscisse du maillage en cours de traitement
Y:=W div G; // ordonnée du maillage en cours de traitement

C:=TAlphaColorRec(CorrectColor(bitmapData.GetPixel(x,y))); // On récupère la couleur du pixel correspondant dans la heightmap
zMap := (C.R  + C.G  + C.B ) / $FF * sizemap / 25; // détermination de la hauteur du sommet en fonction de la couleur

Front^.Z := zMap; // on affecte la hauteur calculée à la face avant
Back^.Z := zMap;  // pareil pour la face arrière
end;

M.CalcTangentBinormals; // Calcul de vecteurs binormaux et de tangente pour toutes les faces (permet par exemple de mieux réagir à la lumière)
mSol.SetSize(sizemap, sizemap, 50);  // Préparation du TMesh
mSol.Data.Assign(M);  // On affecte les données du meshdata précédemment calculées au composant TMesh
finally
SubMap.Unmap(bitmapData);  // On libère le bitmap
end;
end;

finally
FreeAndNil(SubMap);
FreeAndNil(M);
FreeAndNil(Basic);
end;
end;

Enfin, générez l’événement OnDestroy de la fiche fPrincipale pour y libérer maHeightMap.

procedure TfPrincipale.FormDestroy(Sender: TObject);
begin
FreeAndNil(maHeightMap);
end;

Si vous exécutez tel quel le projet, vous allez obtenir un écran tout rouge. C’est normal puisque :

1. La couleur rouge est la couleur par défaut utilisée par Firemonkey lorsque nous n’affectons pas nous-mêmes une couleur à un objet 3D ;

2. L’objet est trop gros par rapport à notre point de vue : nous sommes trop proches de l’objet ;

3. Par défaut, le TMesh n’est pas correctement positionné.

Nous allons donc ajouter un composant TCamera sur la fiche (rubrique « Scène 3D »). Via la vue Structure, nous la plaçons afin qu’elle soit enfant de Viewport. Puis via l’inspecteur d’objets, nous allons la positionner avec sa propriété position.Z à -350 (pour rappel, l’axe Z est l’axe de profondeur : une valeur positive emmène vers le fond de l’écran, une valeur négative emmène derrière le point de vue). Ensuite, il faut indiquer au Viewport que nous souhaitons utiliser notre caméra et qu’il ne doit pas utiliser celle par défaut.

Pour cela, sélectionnez le Viewport et, dans l’inspecteur d’objets, positionnez sa propriété camera à camera1 (normalement c’est déjà fait automatiquement par Delphi). Ensuite, décochez la case UsingDesignCamera.

Enfin, nous allons positionner correctement le TMesh en le sélectionnant, puis, via l’inspecteur d’objets, en renseignant sa propriété RotationAngle.X à 90 afin de le faire pivoter de 90° sur l’axe X. Cela est nécessaire du fait que nous sommes partis d’un TPlane et, par défaut, un TPlane est un carré positionné verticalement.

Maintenant, si nous exécutons le projet, nous obtenons bien une masse rouge devant nous et le fond bleu. Nous distinguons une forme, mais c’est encore assez vague.

Voici le rendu que vous devriez obtenir :

Première chose, pour mieux comprendre l’objet, nous allons forcer le traçage des arêtes des polygones sur notre TMesh. Pour cela, il faut sélectionner le TMesh puis, via l’inspecteur d’objets, générer son événement OnRender.

Entrez le code suivant :

procedure TfPrincipale.mSolRender(Sender: TObject; Context: TContext3D);
begin
// Permet de tracer le maillage du TMesh
Context.DrawLines(mSol.Data.VertexBuffer, mSol.Data.IndexBuffer, TMaterialSource.ValidMaterial(couleurMaillage),0.25);
end;

Cela va tracer les lignes des arêtes des différents polygones en utilisant le TColorMaterialSource couleurMaillage que nous avons posé sur la fiche précédemment et que nous avions configuré avec la couleur bleue.

Exécutons à nouveau le projet. Cette fois-ci, nous obtenons :

Nous constatons maintenant facilement le relief.

III-C - Quelques améliorations

Nous allons ajouter une petite animation de rotation de l’objet mSol. Ajoutons donc un TFloatAnimation (rubrique « Animations ») sur notre fiche. Via la vue Structure, déplaçons le TFloatAnimation afin qu’il soit enfant du dmyMonde. Ensuite, via l’inspecteur d’objets, modifions ses propriétés :

- duration à 20 (afin que la rotation mette 20 secondes pour effectuer un tour) ;

- enabled à true ;

- loop à true (afin que l’animation dure indéfiniment) ;

- PropertyName à RotationAngle.Y ;

- StopValue à 360 (pour faire un tour complet).

Nous aurions pu affecter le TFloatAnimation sur l’objet mSol directement. Attention dans ce cas : nous avons pivoté le mSol de 90° sur l’axe X, donc son repère a également tourné. Pour obtenir le même résultat de rotation, il faudra dans ce cas effectuer la rotation sur l’axe Z du TMesh.

Je préfère placer le TFloatAnimation sur le dmyMonde pour cette raison, mais aussi parce que, au cas où par la suite nous ajouterions des objets au dmyMonde pour compléter la scène 3D (sans que ces derniers soient obligatoirement des enfants du mSol), l’animation les affecterait également.

Exécutez à nouveau le programme : le rendu est similaire à tout à l’heure, mais l’objet tourne à présent sur lui-même.

En l’état, en exécutant le programme, vous remarquerez que le simple fait de passer le curseur de la souris sur le TMesh peut provoquer des ralentissements. Ceux-ci sont dus au fait que des événements sont déclenchés (par exemple le OnMouseMove). Pour éviter cela, et comme nous n’avons pas besoin de cliquer sur le TMesh, nous allons désactiver ce comportement en mettant sa propriété HitTest à false.
Je vous conseille de bien mettre à false la propriété HitTest des objets qui n’ont pas vocation à interagir avec la souris.

Vous pouvez vous amuser à n’afficher que le tracé des polygones. Pour ce faire, il vous suffit de placer un nouveau TColorMaterialSource sur la fiche, de lui affecter la couleur null puis de positionner la propriété MaterialSource du TMesh avec ce nouveau TColorMaterialSource.

Pour préparer la suite, nous allons appliquer une texture à notre objet mSol.

Plaçons donc un composant TTextureMaterialSource (rubrique « Matériaux »), renommons-le en TextureMesh et affectons à sa propriété Texture la texture plan.png.

Ensuite, sélectionnons le composant mSol et affectons TextureMesh à la propriété MaterialSource de mSol.

N’affichons plus le tracé des arêtes des polygones de l’objet mSol : commentons simplement le contenu de la procédure mSolRender.

Enfin, avant de terminer ce premier épisode, nous allons placer la mer autour de notre île.

Pour y parvenir, ajoutons un TPlane sur la fiche que nous renommerons pMer. Via la vue Structure, placez pMer en tant qu’enfant de mSol.

Il nous faut à présent tailler et placer pMer :

- mettez ses propriétés Height et Width à 960. Nous lui donnons ainsi une taille qui déborde largement de mSol ;

- il faut le descendre un petit peu. Fixez par exemple sa propriété position.Z à -22. Vous pouvez placer le niveau de la mer comme bon vous semble.

Plaçons maintenant un nouveau composant TTextureMaterialSource, renommons-le en TextureMer et affectons à sa propriété Texture la texture mer.jpg.

Il ne reste plus qu’à affecter TextureMer à la propriété MaterialSource de pMer.

Exécutez à nouveau le programme et voici le rendu que vous devriez obtenir :

IV - Conclusion de cet épisode et la suite

Nous voici au terme de ce premier épisode : en quelques lignes de code, vous êtes maintenant capables de générer un paysage en 3D. Pas mal, non ?

J’espère que ce premier épisode a éveillé votre curiosité et je vous donne rendez-vous prochainement pour le deuxième épisode où nous apprendrons à nous orienter et nous déplacer dans ce petit monde virtuel.

Vous pouvez retrouver les sources de cet épisode 1 (le fichier zip contient également les textures utilisées) : https://gbegreg.developpez.com/tutoriels/delphi/firemonkey/FMXIsland/episode1/src/episode1_src.zip

V - Remerciements

Je vous remercie d’avoir suivi ce premier épisode. J’espère qu’il vous a plu.

Merci aux relecteurs et correcteurs de ce tutoriel : SergioMaster, gvasseur58 et f-leb.

Enfin, je remercie l'équipe de Developpez.com pour son travail et son soutien.