##### PROJET INFOGRAPHIE 3D : SCRIPT FINAL ### Partie 1 : Construction d'un terrain montagneux ## Fonction hauteursAleatoires import bpy import random import math from mathutils import Euler def hauteursAleatoires(objet, hauteur): """ Cette fonction parcourt tous les sommets de l'objet donné en paramètre et ajoute à la hauteur (composante z) de chaque point une valeur aléatoirement choisie entre -hauteur et hauteur. Paramètres : - objet : un objet polygonal de type mesh. - hauteur : un flottant représentant la hauteur de l'objet. Retour : RIEN """ # Vérifier si l'objet est un objet mesh if objet.type == 'MESH': # Parcours de tous les sommets de l'objet for sommet in objet.data.vertices: # Génération d'une valeur aléatoire entre -hauteur et hauteur valeur_aleatoire = random.uniform(-hauteur, hauteur) # Ajout de la valeur aléatoire à la composante z du sommet sommet.co.z += valeur_aleatoire else: print("L'objet n'est pas un objet mesh.") ## Fonction subdiviseAretes def subdiviseAretes(objet): """ Cette fonction subdivise chaque face de l'objet donné en paramètre. Cette subdivision consiste à insérer un sommet au milieu de chaque arête et de chaque face. Ainsi chaque face est transformée en quatre faces coplanaires. Paramètres : - objet : un objet polygonal de type mesh. Retour : RIEN """ # Vérifier si l'objet est un objet mesh if objet.type == 'MESH': bpy.ops.object.mode_set(mode='EDIT') # Passage en mode édition # Sélection de toutes les arêtes bpy.ops.mesh.select_all(action='SELECT') # Subdivision des arêtes bpy.ops.mesh.subdivide(number_cuts=1, smoothness=0) bpy.ops.object.mode_set(mode='OBJECT') # Revenir en mode objet else: print("L'objet n'est pas un objet mesh.") ## Fonction creerLac def creerLac(largeur): """ Cette fonction crée un plan ayant une face dont le nom est 'lac' et qui représente la surface d'un lac. Paramètres : - largeur : un flottant représentant la largeur du plan (et donc du lac) à créer. Retour : l'objet créé par la fonction (donc le lac). """ # Création d'un plan pour représenter la surface du lac bpy.ops.mesh.primitive_plane_add(size=largeur, enter_editmode=False, align='WORLD', location=(0, 0, 0)) # Récupération de l'objet actif (le plan créé) lac = bpy.context.active_object # Renommage de la face du lac sous le nom 'lac' lac.data.polygons[0].use_smooth = True # Rendre la face lisse (pas de subdivision nécessaire) lac.data.polygons[0].material_index = 0 # Affecter un matériau à la face (utile pour la partie 1.2) lac.name = "lac" # Retourner l'objet créé return lac ## Fonction creerCiel def creerCiel(rayon): """ Cette fonction crée une sphère dont le nom est 'ciel' et qui représente le ciel. Paramètres : - rayon : un flottant représentant le rayon de la sphère (qui représente le ciel) à créer. Retour : l'objet créé par la fonction (donc la sphère qui représente le ciel). """ # Création d'une sphère pour représenter le ciel bpy.ops.mesh.primitive_uv_sphere_add(radius=rayon, location=(0, 0, 0)) # Récupération de l'objet actif (la sphère créée) ciel = bpy.context.active_object # Renommage de la sphère sous le nom 'ciel' ciel.name = "ciel" # Désactiver la visibilité de l'ombre pour cet objet # ciel.cycles_visibility.shadow = False # Retourner l'objet créé return ciel ## Fonction creerMontagne def creerMontagne(nom, largeur, hauteur, Nsubdiv): """ Cette fonction, crée une montagne de largeur 'largeur', de hauteur approximative 'hauteur' et dont la résolution est déterminée par 'Nsubdiv'. Paramètres : - nom : une chaîne de caractères représentant le nom de l'objet à créer. - largeur : un nombre représentant la largeur de l'objet à créer. - hauteur : un nombre représentant la hauteur de l'objet à créer. - Nsubdiv : un entier représentant le nombre de subdivisions à effectuer lors de la création de la montagne. Retour : l'objet créé par la fonction (donc la montagne). """ # Création d'un plan de taille largeur bpy.ops.mesh.primitive_plane_add(size=largeur, enter_editmode=False, align='WORLD', location=(0, 0, 0)) # Récupération de l'objet actif (le plan créé) montagne = bpy.context.active_object # Renommage de l'objet avec le nom donné en paramètre montagne.name = nom # Répéter Nsubdiv fois for _ in range(Nsubdiv): hauteursAleatoires(montagne, hauteur) # Appliquer des hauteurs aléatoires subdiviseAretes(montagne) # Subdiviser les arêtes de l'objet hauteur /= 2 # Diminuer la hauteur à chaque itération # Retourner l'objet créé return montagne ## Fonction raccrocheMats def raccrocheMats(): """ Cette fonction parcourt tous les objets de la scène et attache les matériaux appropriés aux objets correspondants en fonction de leur nom. """ # Rechercher et stocker les matériaux par leur nom dans des variables mat_montagne = bpy.data.materials.get('mat_montagne') # Matériau pour les montagnes mat_montagne_2 = bpy.data.materials.get('mat_montagne_2') # Matériau pour les montagnes de fond mat_ciel = bpy.data.materials.get('mat_ciel') # Matériau pour le ciel mat_eau = bpy.data.materials.get('mat_eau2') # Matériau pour l'eau du lac # Parcourir tous les objets présents dans la scène actuelle for obj in bpy.data.objects: # Vérifier si l'objet est une montagne (en fonction de son nom) if "MONTAGNE" in obj.name.upper(): # Si le matériau de la montagne existe et que l'objet est de type mesh if mat_montagne and obj.type == 'MESH': # Si l'objet n'a pas de matériau, ajouter le matériau 'mat_montagne' if not obj.data.materials: obj.data.materials.append(mat_montagne) # Sinon, remplacer le matériau existant par 'mat_montagne' else: obj.data.materials[0] = mat_montagne # Vérifier si l'objet est une montagne de fond (en fonction de son nom) if "FOND" in obj.name.upper(): # Si le matériau de la montagne existe et que l'objet est de type mesh if mat_montagne_2 and obj.type == 'MESH': # Si l'objet n'a pas de matériau, ajouter le matériau 'mat_montagne_2' if not obj.data.materials: obj.data.materials.append(mat_montagne_2) # Sinon, remplacer le matériau existant par 'mat_montagne_2' else: obj.data.materials[0] = mat_montagne_2 # Vérifier si l'objet est le ciel et si le matériau du ciel existe elif obj.name == "ciel" and mat_ciel: # Si l'objet n'a pas de matériau, ajouter le matériau 'mat_ciel' if not obj.data.materials: obj.data.materials.append(mat_ciel) # Sinon, remplacer le matériau existant par 'mat_ciel' else: obj.data.materials[0] = mat_ciel # Vérifier si l'objet est le lac et si le matériau de l'eau existe elif obj.name == "lac" and mat_eau: # Si l'objet n'a pas de matériau, ajouter le matériau 'mat_eau2' if not obj.data.materials: obj.data.materials.append(mat_eau) # Sinon, remplacer le matériau existant par 'mat_eau2' else: obj.data.materials[0] = mat_eau ### Partie 3 : Répartition d’objets simples sur un polygone ## Fonction repartitionUniforme def repartitionUniforme(nom_objet, N, zmin, zmax): """ Cette fonction retourne une liste de N triplets représentant des sommets choisis au hasard sur la surface de l'objet. La hauteur de ces positions est comprise entre zmin et zmax (zmin < zmax). Paramètres : - nom_objet : chaîne de caractères représentant le nom de l'objet polygonal. - N : entier positif représentant le nombre de positions à générer. - zmin : nombre représentant la hauteur minimale. - zmax : nombre représentant la hauteur maximale. Retour : une liste de N triplets (x, y, z) représentant les positions générées. """ # Récupérer l'objet par son nom objet = bpy.data.objects.get(nom_objet) # Vérifier si l'objet existe et est de type mesh if objet and objet.type == 'MESH': # Création de la liste 'positions' dans laquelle les N triplets (x, y, z) seront enregistrés positions = [] # Obtention d'une liste de tous les sommets de l'objet sommets = objet.data.vertices # Boucle jusqu'à atteindre le nombre souhaité de positions while len(positions) < N: # Choisir un sommet au hasard sommet = random.choice(sommets) # Récupérer les coordonnées du sommet (transforme les coordonnées locales en coordonnées globales) x, y, z = objet.matrix_world @ sommet.co # Vérifier si la hauteur est dans la plage de hauteur spécifiée if zmin <= z <= zmax: positions.append((x, y, z)) # Ajoute le triplet de coordonnées à la liste # Retourner la liste de N triplets return positions else: print("L'objet n'existe pas ou n'est pas de type mesh.") return None ## Fonction repartitionUniforme2 (version modifiée pour que chaque sommet ne soit choisi qu'une seule fois) def repartitionUniforme2(nom_objet, N, zmin, zmax): """ Cette fonction admet quatre paramètres : nom_objet, un entier positif N et deux nombres zmin et zmax (avec zmin < zmax). Elle retourne une liste de N triplets représentant des sommets choisis au hasard sur la surface de l'objet. La hauteur de ces positions devra être comprise entre zmin et zmax. Paramètres : - nom_objet : chaîne de caractères représentant le nom de l'objet polygonal. - N : entier positif représentant le nombre de positions à générer. - zmin : nombre représentant la hauteur minimale. - zmax : nombre représentant la hauteur maximale. Retour : une liste de N triplets (x, y, z) représentant les positions générées. """ # Récupérer l'objet par son nom objet = bpy.data.objects.get(nom_objet) # Vérifier si l'objet existe et est de type mesh if objet and objet.type == 'MESH': positions = set() # Utiliser un ensemble pour éviter les doublons # Obtention d'une liste de tous les sommets de l'objet sommets = objet.data.vertices # Boucle jusqu'à atteindre le nombre souhaité de positions while len(positions) < N: # Choisir un sommet au hasard sommet = random.choice(sommets) # Récupérer les coordonnées du sommet (transforme les coordonnées locales en coordonnées globales) x, y, z = objet.matrix_world @ sommet.co # Vérifier si la hauteur est dans la plage spécifiée et si le sommet n'a pas déjà été choisi if zmin <= z <= zmax and (x, y, z) not in positions: positions.add((x, y, z)) # Ajoute le triplet de coordonnées à l'ensemble # Vérifier si le nombre de positions trouvées est inférieur à N, ce qui signifierait qu'il n'y a pas assez de sommets valides pour répondre à la demande. if len(positions) < N: # Afficher un message d'avertissement : le nombre de sommets trouvés est inférieur à celui demandé print(f"Attention : Seulement {len(positions)} sommets disponibles entre zmin et zmax.") # Convertir l'ensemble 'positions' en liste return list(positions) # Si l'objet spécifié n'existe pas ou n'est pas un mesh. else: print("L'objet n'existe pas ou n'est pas de type mesh.") return None ## Fonction pileOuFaceProbaP def pileOuFaceProbaP(p): """ Cette fonction retourne aléatoirement 'True' ou 'False' avec une probabilité p d'obtenir 'True'. Exemples : - pour p = 0, la fonction retourne toujours 'False' ; - pour p = 1, la fonction retourne toujours 'True' ; - pour p = 0.5, la fonction retourne 'True' ou 'False' avec la même probabilité ; - pour p = 0.8, la fonction retourne 'True' avec une probabilité de 80% et 'False' avec une probabilité de 20%. Paramètre : un nombre flottant p entre 0 et 1 représentant la probabilité d'obtenir 'True'. Retour : 'True' avec une probabilité p, et 'False' avec une probabilité (1 - p). """ return random.random() < p # Générer un nombre aléatoire entre 0 et 1 et le compare à p ## Fonction coefZ0 def coefZ0(zmin, zmax): """ Cette fonction calcule la valeur de z0 en se basant sur le fait que la densité en zmin doit être 100 fois supérieure à celle en zmax. Paramètres : - zmin : un nombre représentant la valeur minimale de z (hauteur minimale). - zmax : un nombre représentant la valeur maximale de z (hauteur maximale). Retour : la valeur z0 du paramètre déterminant la rapidité de la décroissance exponentielle. """ # Explication du calcul : # On sait que la densité de population 'densite(z)' suit une décroissance exponentielle de la forme : Ae^(-(z / z0)) avec A une constante # et z0 le paramètre que l'on cherche à déterminer. # La densité à zmin doit être 100 fois plus grande que celle à zmax. Donc : densite(zmin) = 100 x densite(zmax). # On substitue densite(z) par Ae^(-(z / z0)) on obtient : Ae^(-(zmin / z0)) = 100 x Ae^(-(zmax / z0)). # On simplifie l'équation par A : e^(-(zmin / z0)) = 100 x e^(-(zmax / z0)). # En utilisant la propriété e^x / e^y = e^(x - y) ; donnée dans l'énoncé ; on reformule l'équation : e^(-(zmin / z0) + (zmax / z0)) = 100. # On réarrange l'équation : e^((zmax - zmin) / z0) = 100. # En utilisant la propriété que y = e^x si et seulement si x = ln(y) ; donnée dans l'énoncé ; on prend le logarithme naturel des deux côtés. # On obtient : (zmax - zmin) / z0 = ln(100). # Finalement, on isole z0 pour trouver : z0 = (zmax - zmin) / ln(100) # On retourne la valeur de z0 (explication du calcul ci-dessus) return (zmax - zmin) / math.log(100) ## Fonction coefA def coefA(nom_objet, N, z0, zmin, zmax): """ Cette fonction calcule le coefficient A pour une distribution de population avec densité décroissante. Paramètres : - nom_objet : une chaîne de caractères représentant le nom de l'objet polygonal. - N : un entier représentant le nombre total d'individus à répartir. - z0 : un nombre définissant la rapidité de la décroissance de la densité. - zmin : un nombre représentant la valeur minimale de z (hauteur minimale). - zmax : un nombre représentant la valeur maximale de z (hauteur maximale). Retour : le coefficient A déterminant la densité de population. """ # Récupérer l'objet par son nom objet = bpy.data.objects.get(nom_objet) # Vérifier si l'objet existe et est de type mesh if objet and objet.type == 'MESH': # Initialiser la somme des exponentielles à 0 somme_exp = 0 # Parcourir chaque sommet de l'objet for sommet in objet.data.vertices: # Convertir les coordonnées locales du sommet en coordonnées globales x, y, z = objet.matrix_world @ sommet.co # Vérifier si le sommet se trouve dans la plage de hauteur spécifiée if zmin <= z <= zmax: # Ajouter e^(-zi/z0) à la somme si le sommet est dans la plage de hauteur somme_exp += math.exp(-z / z0) # Calcul du coefficient A en utilisant la relation donnée # A = N / somme_exp avec 'somme_exp' la somme des e^(-zi/z0) pour tous les sommets valides A = N / somme_exp if somme_exp != 0 else 0 # Eviter la division par zéro # Retourner la valeur du coefficient A qui détermine la densité de population return A else: print("L'objet n'existe pas ou n'est pas de type mesh.") return None # RELATION 2 : établit que la somme des densités à chaque sommet (pondérée par le coefficient A et ajustée par l'exponentielle décroissante) doit égaler la population totale N. # RELATION 3 : permet de calculer le coefficient A en fonction de la population totale N et de la somme des valeurs exponentielles des hauteurs des sommets. # Elle est utilisée pour ajuster la densité globale de sorte que la population totale sur l'objet soit égale à N. ## Fonction repartitionNonUniforme def repartitionNonUniforme(nom_objet, N, coef_A, coef_z0, zmin, zmax): """ Cette fonction retourne une liste de N triplets représentant des positions de sommets sur l'objet. Ces positions sont choisies en fonction d'une densité de population qui décroît exponentiellement. Paramètres : - nom_objet : une chaîne de caractères représentant le nom de l'objet polygonal. - N : un entier représentant le nombre de positions à trouver. - coef_A : un nombre représentant le coefficient A pour la densité. - coef_z0 : un nombre représentant le coefficient z0 pour la densité. - zmin : un nombre représentant la valeur minimale de z (hauteur minimale). - zmax : un nombre représentant la valeur maximale de z (hauteur maximale). Retour : - Liste de N triplets (x, y, z) représentant les positions choisies. """ # Récupérer l'objet par son nom objet = bpy.data.objects.get(nom_objet) # Initialiser une liste pour stocker les positions choisies positions = [] # Vérifier si l'objet existe et est de type mesh if objet and objet.type == 'MESH': # Parcourir chaque sommet de l'objet for sommet in objet.data.vertices: # Convertir les coordonnées locales du sommet en coordonnées globales x, y, z = objet.matrix_world @ sommet.co # Vérifier si le sommet se trouve dans la plage de hauteur spécifiée if zmin <= z <= zmax: # Calculer la densité à ce sommet densite = coef_A * math.exp(-z / coef_z0) # Utiliser la fonction pileOuFaceProbaP avec la densité calculée if pileOuFaceProbaP(densite): # Ajouter la position à la liste si pileOuFaceProbaP retourne True positions.append((x, y, z)) # Arrêter la boucle une fois que N positions sont trouvées if len(positions) >= N: break # Retourner la liste des positions trouvées return positions else: print("L'objet n'existe pas ou n'est pas de type mesh.") return None ## Fonction peuplement def peuplement(positions, noms_plaques, camera): """ Place des objets représentés par des plans (plaques) avec des matériaux prédéfinis à des positions spécifiées dans une scène 3D. Ces objets sont ensuite ajustés pour faire face à la caméra et sont redimensionnés pour maintenir la cohérence visuelle en fonction de la distance à la caméra. """ # Obtenir et stocker la position de la caméra pour éviter de multiples accès à l'objet caméra camera_location = camera.location # Itérer sur chaque position fournie dans la liste des positions for position in positions: # Créer une nouvelle plaque (plan) à la position spécifiée avec une taille prédéfinie bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=position) # Récupérer l'objet actif après l'ajout (la plaque qui vient d'être créée) plaque = bpy.context.active_object # Sélectionner un matériau aléatoirement dans la liste des matériaux fournis nom_plaque = random.choice(noms_plaques) # Récupérer l'objet matériel à partir du nom sélectionné mat = bpy.data.materials.get(nom_plaque) # Si la plaque a déjà des matériaux, remplacer le premier par le matériau sélectionné if len(plaque.data.materials): plaque.data.materials[0] = mat # Sinon, ajouter le matériau sélectionné à la liste des matériaux de la plaque else: plaque.data.materials.append(mat) # Calculer le vecteur direction de la plaque à la caméra pour l'orientation et le redimensionnement direction = camera_location - plaque.location # Calculer la distance de la plaque à la caméra en utilisant la longueur du vecteur direction distance_to_camera = direction.length # Calculer un facteur d'échelle pour la plaque basé sur la distance à la caméra # Les objets plus proches apparaîtront plus grands en utilisant une formule d'échelle inverse scale_factor = 45.0 / distance_to_camera # S'assurer que le facteur d'échelle ne rende pas la plaque trop grande scale_factor = max(scale_factor, 0.1) # Appliquer le facteur d'échelle aux dimensions de la plaque pour modifier sa taille plaque.scale = (scale_factor, scale_factor, scale_factor) # Ajuster l'orientation de la plaque pour qu'elle fasse face à la caméra en utilisant la direction calculée plaque.rotation_euler = direction.to_track_quat('Z', 'Y').to_euler() # Mise à jour de la scène pour que les changements prennent effet immédiatement bpy.context.view_layer.update() ### Partie 4 : Synthèse # Liste des noms de matériaux pour les plaques noms_plaques_environnement = [ 'plaque_arbre1', 'plaque_arbre3', 'plaque_arbre4', 'plaque_arbre5', 'plaque_arbre6', 'plaque_buisson1', ] ## Fonction principale pour créer la scène complète def creerSceneMontagne(): """ Cette fonction crée une scène 3D représentant une montagne avec un lac, une sphère représentant le ciel, une caméra et une source de lumière. Paramètres : aucun. Cette fonction appelle certaines des fonctions définies précédemment (creerLac, creerCiel, creerMontagne). Retour : aucun. Cette fonction crée et configure tous les éléments de la scène mais ne retourne pas d'objet en tant que tel. """ # Créer le ciel ciel_rayon = 100 ciel = creerCiel(ciel_rayon) # Créer le lac largeur_lac = 50 lac = creerLac(largeur_lac) # Créer la première montagne (principale) seed_montagne1 = 21343 # Graine de la première montagne largeur_montagne1 = 60 # Augmenter la largeur hauteur_montagne1 = 45 # Augmenter la hauteur Nsubdiv = 7 random.seed(seed_montagne1) montagne1 = creerMontagne("Montagne1", largeur_montagne1, hauteur_montagne1, Nsubdiv) montagne1.location = (5, -0.71366, 0) # Créer une deuxième montagne (de fond) seed_montagne2 = 76543 largeur_montagne2 = 80 hauteur_montagne2 = 50 random.seed(seed_montagne2) montagne2 = creerMontagne("Montagne2", largeur_montagne2, hauteur_montagne2, Nsubdiv) montagne2.location = (10.6142, -46.808, 25.64) # Créer une troisième montagne (de fond) seed_montagne3 = 12781 largeur_montagne3 = 40 hauteur_montagne3 = 50 random.seed(seed_montagne3) montagne3 = creerMontagne("Montagne3", largeur_montagne3, hauteur_montagne3, Nsubdiv) montagne3.location = (16.743, -65.553, 14.637) # Créer une quatrième montagne (de fond) seed_montagne4 = 28961 largeur_montagne4 = 40 hauteur_montagne4 = 50 random.seed(seed_montagne4) montagne4 = creerMontagne("Fond1", largeur_montagne4, hauteur_montagne4, Nsubdiv) montagne4.location = (54.291, -49.875, 3.5203) montagne4.rotation_euler = (0, 0, math.radians(-54)) # Créer une cinquième montagne (de fond) seed_montagne5 = 17933903 largeur_montagne5 = 40 hauteur_montagne5 = 50 random.seed(seed_montagne5) montagne5 = creerMontagne("Fond2", largeur_montagne5, hauteur_montagne4, Nsubdiv) montagne5.location = (53.232, 5.0312, 0) rotation_z = math.radians(-54) # Convertir -80 degrés en radians montagne5.rotation_euler = (0, 0, math.radians(-85.718)) # Créer la caméra dans l'angle de vue choisi position_camera = (-6.9776, 25.654, 8.4649) # Valeurs récupérées pour la position rotation_camera = (math.radians(82), 0, math.radians(218)) # Valeurs récupérées pour la rotation # Ajouter une caméra à la scène bpy.ops.object.camera_add() camera = bpy.context.active_object # Définir la position et la rotation de la caméra camera.location = position_camera camera.rotation_euler = rotation_camera # Définir la longueur focale de la caméra camera.data.lens = 22.5 # Valeur récupérée pour la longueur focale # Créer une lumière principale (Area Light) qui éclaire toute la scène bpy.ops.object.light_add(type='AREA', location=(0, 0, 50)) lumiere_principale = bpy.context.active_object lumiere_principale.data.energy = 5.0 # Intensité plus élevée pour éclairer toute la scène lumiere_principale.data.color = (1, 1, 1) # Couleur blanche lumiere_principale.data.size = 10.0 # Taille pour un éclairage large # Créer des lumières d'appoint pour les montagnes for i, montagne in enumerate([montagne1, montagne2, montagne3, montagne4]): bpy.ops.object.light_add(type='POINT', location=montagne.location) lumiere_montagne = bpy.context.active_object lumiere_montagne.data.energy = 0.5 # Moins intense que la lumière principale lumiere_montagne.data.color = (0.8, 0.8, 0.8) # Légèrement jaune # Créer une lumière réfléchissante pour le lac bpy.ops.object.light_add(type='SPOT', location=(0, 0, 10)) lumiere_lac = bpy.context.active_object lumiere_lac.data.energy = 0.3 lumiere_lac.data.color = (0.5, 0.5, 1.0) # Couleur bleutée pour l'eau lumiere_lac.data.spot_size = math.radians(45) # Ajuster l'angle du spot raccrocheMats() # Créer des plaques représentant des arbres (placées aléatoirement) N_plaques = 100 # Nombre de plaques à placer zmin, zmax = 0, hauteur_montagne1 / 2 # Limites d'altitude # Calcul des coefficients pour la répartition non uniforme coef_z0 = coefZ0(zmin, zmax) coef_A = coefA("Montagne1", N_plaques, coef_z0, zmin, zmax) # Obtenir les positions pour les plaques positions_plaques = repartitionNonUniforme("Montagne1", N_plaques, coef_A, coef_z0, zmin, zmax) # Placer les plaques sur les montagnes peuplement(positions_plaques, noms_plaques_environnement, camera) # Créer des plaques représentant des animaux (placées avec des coordonnées précises) # Liste avec les coordonnées personnalisées coordonnees_personnalisees = [ (19.633, 7.7721, 3.4302), (15.656, -4.2006, 2.796), (7.6063, -2.2183, 1.493), (31.796, -1.9293, 14.234), (19.626, -21.711, 3.1553), (34.586, 4.1752, 14.676), (43.666, -42.969, 6.5507) ] # Liste des noms de matériaux correspondants aux plaques pour les animaux noms_materiaux_personnalises = [ 'plaque_renard1', 'plaque_cerf1', 'plaque_bouquetin3', 'plaque_loup1' ] # Utiliser la fonction peuplement pour créer des plaques aux coordonnées personnalisées peuplement(coordonnees_personnalisees, noms_materiaux_personnalises, camera) # Exemple d'utilisation creerSceneMontagne()