La visualisation de données est née avec les cartes préhistoriques et les marques de comptage, évoluant à travers les premiers graphiques statistiques créés par des personnalités comme William Playfair au 18e siècle, et les visualisations cruciales de données médicales par John Snow au 19e siècle. Les développements clés incluent les diagrammes en rose de Florence Nightingale pendant la guerre de Crimée, les travaux fondamentaux de statisticiens comme John Tukey et Jacques Bertin dans les années 1960-70, et la révolution numérique qui a rendu le traitement rapide des données et les visualisations interactives largement accessibles.
Origines préhistoriques et antiques
Premiers outils :
Les premières formes de visualisation de données étaient des outils primitifs pour compter et naviguer, comme les marques de comptage sur des os (par exemple, l’os d’Ishango il y a 20 000 ans) et les premières cartes.
Le 17e siècle a vu l’émergence de la visualisation statistique, avec des personnalités comme Michael Florent van Langren produisant les premières représentations de données statistiques.
Au 18e siècle, des figures comme William Playfair ont introduit des graphiques et diagrammes révolutionnaires pour visualiser les données économiques.
Le 19e siècle : Percées médicales et perspectives sociologiques
Épidémie de choléra :
En 1854, le Dr John Snow a créé une carte d’une épidémie de choléra à Londres, un moment crucial dans l’utilisation de la visualisation de données pour résoudre des problèmes concrets et influencer la santé publique.
Florence Nightingale :
Pendant la guerre de Crimée, Nightingale a utilisé des “diagrammes en rose” pour illustrer que davantage de soldats mouraient de maladies dans les hôpitaux que sur le champ de bataille, influençant la réforme hospitalière.
W.E.B. Du Bois :
Le début du 20e siècle a vu le travail pionnier de Du Bois, qui a utilisé des visualisations complexes dans son travail sociologique pour mettre en évidence les disparités raciales en Amérique.
Le 20e siècle : L’essor de la théorie scientifique et de l’informatique
Des chercheurs comme John Tukey et Jacques Bertin ont développé les bases scientifiques de la visualisation de données dans les années 1960 et 1970, faisant progresser les graphiques statistiques et la cartographie.
Edward Tufte :
Le livre influent de Tufte, “The Visual Display of Quantitative Information”, publié en 1983, a contribué à établir la visualisation de données comme domaine d’étude.
La fin du 20e siècle et le début du 21e siècle ont vu l’émergence d’outils et de plateformes logiciels sophistiqués, conduisant à des visualisations interactives et des tableaux de bord de données utilisés dans le journalisme, les entreprises et les gouvernements.
Activer Developper Extras et Python Tooltips dans Blender
Dans Blender, choisir Edit→Preferences et dans interface, activer “Developper Extras” et “Python Tooltips”
Blender Workspace pour Python
Créer un nouveau fichier .blend
Ajouter un espace de travail horizontal en haut de type “Scripting → Info”
Ajouter un espace de travail vertical à droite de type “Scripting → Text Editor”
Changer le timeline pour l’espace de travail “Scripting → Python Console”
Pour le reste du cours on va jouer à créer des primitives
Dans le viewport, faites ⇧ A et créez par exemple un cube
Qule code Python apparaît dans l’espace “Info”?
Effacer le cube, copiez et coller le code de création dans la console Python, changez un peu les paramètres de Localtion et Scale et faites ↩
Essayez d’autres actions et jouer avec la console
Dans l’espace text Editor, assurez-vous d’activer Line numbers, Word Wrap et Syntax Highlighting. Cliquez également sur New et tapez le nom du fichier python “AddObject.py”
Choisir le menu Text, puis Save-As et sauvegarder le code dans le dossier cours1
Sauvegarder également le fichier Blender sous le nom AddObject.blend dans le même dossier cours1
💡
Note: le code dans le script editor est sauvegardé avec le fichier .blend, mais il est bon d’en avoir une copie externe dans un fichier .py, donc faire régulièrement, dans l’espace de travail Text Editor: Text → Save
Tapez les instructions suivantes dans le text editor:
import bpyfor i in range (1,10): bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(i, i/2, i), scale=(0.2, i/5, 1))
Amusez-vous à essayer toute sortes de trucs et machins
À la fin du cours, n’oubliez pas de faire Text → Save du fichier .py dans l’espace de travail Text Editor et File → Save du fichier .blend
import bpy# Select all objects in the scenebpy.ops.object.select_all(action='SELECT')# Delete all selected objectsbpy.ops.object.delete()## Create Cubes#for i in range (1,10): bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(i, i/2, i), scale=(0.2, i/5, 1))
## Random Objects#import bpyimport random# Clear the current scene by selecting and deleting all objectsbpy.ops.object.select_all(action='SELECT')bpy.ops.object.delete()# Define the number of primitives to create along X and Y axesgrid_size_x = 5grid_size_y = 5spacing = 3# Create primitives in a gridfor i in range(grid_size_x): for j in range(grid_size_y): # Randomly select a primitive type to add primitive_type = random.choice(['CUBE', 'SPHERE', 'CONE', 'TORUS']) # Calculate the position for the primitive x = i * spacing y = j * spacing z = random.uniform(0, 5) # Random height # Add the chosen primitive type if primitive_type == 'CUBE': bpy.ops.mesh.primitive_cube_add(size=1, location=(x, y, z)) elif primitive_type == 'SPHERE': bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(x, y, z)) elif primitive_type == 'CONE': bpy.ops.mesh.primitive_cone_add(radius1=1, depth=2, location=(x, y, z)) elif primitive_type == 'TORUS': bpy.ops.mesh.primitive_torus_add(location=(x, y, z)) # Randomize the rotation of the object obj = bpy.context.object obj.rotation_euler = (random.uniform(0, 3.14), random.uniform(0, 3.14), random.uniform(0, 3.14)) # Add a random scale factor to make each object unique scale_factor = random.uniform(0.5, 2) obj.scale = (scale_factor, scale_factor, scale_factor)# Adjust camerabpy.ops.object.camera_add(location=(10, -15, 10))camera = bpy.context.objectcamera.rotation_euler = (1.1, 0, 0.8)# Adjust lightingbpy.ops.object.light_add(type='POINT', radius=1, location=(10, 10, 10))light = bpy.context.objectlight.data.energy = 1000
## Sinusoïdale#import bpyimport math# Clear the current scene by selecting and deleting all objectsbpy.ops.object.select_all(action='SELECT')bpy.ops.object.delete()# Animation settingsframe_start = 1frame_end = 250bpy.context.scene.frame_start = frame_startbpy.context.scene.frame_end = frame_end# Define the grid size and spacinggrid_size_x = 10grid_size_y = 10spacing = 2# Create a list to store the spheresspheres = []# Create spheres in a grid patternfor i in range(grid_size_x): for j in range(grid_size_y): # Add a UV sphere bpy.ops.mesh.primitive_uv_sphere_add(radius=0.5, location=(i * spacing, j * spacing, 0)) sphere = bpy.context.object spheres.append(sphere)# Animate the spheres with a wave effectfor frame in range(frame_start, frame_end + 1): bpy.context.scene.frame_set(frame) # Loop through each sphere and set its location based on a sine wave for index, sphere in enumerate(spheres): i = index // grid_size_x j = index % grid_size_x # Calculate the wave height based on sine function (wave-like motion) wave_height = math.sin((frame / 10) + (i + j) / 2) new_location = (i * spacing, j * spacing, wave_height * 2) # Update sphere location sphere.location = new_location # Insert a keyframe for the location sphere.keyframe_insert(data_path="location", index=-1)# Adjust camera to have a good view of the animationbpy.ops.object.camera_add(location=(12, -20, 15))camera = bpy.context.objectcamera.rotation_euler = (1.1, 0, 0.8)# Set active camerabpy.context.scene.camera = camera# Add lighting to the scenebpy.ops.object.light_add(type='SUN', location=(15, -10, 20))light = bpy.context.objectlight.data.energy = 5
## Petit robot avec Material#import bpy# Clear the current scene by selecting and deleting all objectsbpy.ops.object.select_all(action='SELECT')bpy.ops.object.delete()# Function to create a material with a given colordef create_material(name, color): mat = bpy.data.materials.new(name=name) mat.diffuse_color = color # RGBA values return mat# Create materials for different parts of the robotbody_material = create_material("BodyMaterial", (0.1, 0.5, 0.8, 1)) # Blueishhead_material = create_material("HeadMaterial", (0.8, 0.8, 0.8, 1)) # Light Greyeye_material = create_material("EyeMaterial", (0, 0, 0, 1)) # Blackarm_leg_material = create_material("ArmLegMaterial", (0.3, 0.3, 0.3, 1)) # Dark Greyantenna_material = create_material("AntennaMaterial", (1, 0, 0, 1)) # Red# Create the robot bodybpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 1))body = bpy.context.objectbody.scale = (1, 0.6, 1.5) # Reshape the cube to form the bodybody.data.materials.append(body_material) # Apply material# Create the robot headbpy.ops.mesh.primitive_uv_sphere_add(radius=0.75, location=(0, 0, 3.25))head = bpy.context.objecthead.data.materials.append(head_material) # Apply material# Create the robot eyesfor eye_location in [(-0.3, 0.4, 3.5), (0.3, 0.4, 3.5)]: bpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=eye_location) eye = bpy.context.object eye.data.materials.append(eye_material) # Apply material# Create the robot armsfor arm_location in [(-1.5, 0, 1.5), (1.5, 0, 1.5)]: bpy.ops.mesh.primitive_cylinder_add(radius=0.2, depth=2, location=arm_location) arm = bpy.context.object arm.rotation_euler = (0, 1.57, 0) # Rotate to align properly arm.data.materials.append(arm_leg_material) # Apply material# Create the robot legsfor leg_location in [(-0.5, 0, 0), (0.5, 0, 0)]: bpy.ops.mesh.primitive_cube_add(size=0.5, location=leg_location) leg = bpy.context.object leg.scale = (0.3, 0.3, 1) # Reshape to form legs leg.data.materials.append(arm_leg_material) # Apply material# Create the robot antennabpy.ops.mesh.primitive_cylinder_add(radius=0.05, depth=1.5, location=(0, 0, 4.5))antenna = bpy.context.objectantenna.data.materials.append(antenna_material) # Apply material# Create the antenna top spherebpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(0, 0, 5.25))antenna_top = bpy.context.objectantenna_top.data.materials.append(antenna_material) # Apply material# Set up lightingbpy.ops.object.light_add(type='AREA', radius=5, location=(5, -5, 6))light = bpy.context.objectlight.data.energy = 1000 # Increase light intensity# Set up camerabpy.ops.object.camera_add(location=(7, -7, 5))camera = bpy.context.objectcamera.rotation_euler = (1.1, 0, 0.8)# Set the camera as the active camerabpy.context.scene.camera = camera
## Fibonnaci#import bpyimport math# Clear the current scene by selecting and deleting all objectsbpy.ops.object.select_all(action='SELECT')bpy.ops.object.delete()spheres = []def generateSunflowerSpiral(numPoints=1000, spacing=0.4, zHeight=0): points = [] goldenAngle = 137.5 * (math.pi / 180.0); for i in range(numPoints): angle = i * goldenAngle; radius = spacing * math.sqrt(i); x = radius * math.cos(angle); y = radius * math.sin(angle); points.append((x, y, zHeight)); return points;points = generateSunflowerSpiral()for x,y,z in points: # Add a UV sphere bpy.ops.mesh.primitive_uv_sphere_add(radius=0.2, location=(x,y,z)) sphere = bpy.context.object spheres.append(sphere)
Télécharager et placer le fichier CSV dans un dossier de votre choix.
Dans ce même dossier, copier le fichier .blend du cours 1 (pour avoir le workspace adéquat) et nommez-le arbresYUL.blend
Ouvrez le fichier Blender arbresYUL.blend en double-cliquant dessus (sinon le cwd sera celui du l’executable Blender)
Assurez-vous d’ouvrir également la console (Window → Toggle System Console)
Code, itération 1 - Lecture du fichier CSV
import osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## Lecture du CSV#pprint.pprint("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)pprint.pprint(trees_csv[:10])
Code, itération 2 - Triage et regroupement des données
import osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv[:10])# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)
Code, itération 3 - Affichage des barres
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale
Code, itération 4 - Ajout du texte et des textures
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary kesTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale # Create a material for the bar based on the TOTAL value (color from red to green) mat = bpy.data.materials.new(name=f"BarMaterial_{i}") color_intensity = total_norm # Use normalized TOTAL for color intensity mat.diffuse_color = (1 - color_intensity, color_intensity, 0, 1) # Transition from red to green bar.data.materials.append(mat) # Create text for essence bpy.ops.object.text_add(location=(-0.5, -i * spacing, 0)) # Place the text to the left of the bar text_obj = bpy.context.object text_obj.data.body = essence text_obj.data.align_x = 'RIGHT' # Create text for the total label bpy.ops.object.text_add(location=(0.5, -i * spacing - spacing / 4, bar_height/2)) # Place the text to the right of the bar text_obj = bpy.context.object text_obj.data.body = str(total) text_obj.data.align_x = 'LEFT' text_obj.scale = (0.5, 0.5, 0.5) bpy.ops.transform.translate(value=(total_norm * scale * 2, 0, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False)
Code, itération 5 - Animation
import bpyimport osfrom datetime import datetimeimport csvimport collectionsimport pprintimport math# Montreal Trees CSV fileTREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")# CSV data set columnsCOL = { 'INV_TYPE': 0, 'EMP_NO': 1, 'ARROND': 2, 'ARROND_NOM': 3, 'Rue': 4, 'Rue_cote': 5, 'No_civique': 6, 'Emplacement': 7, 'Sigle': 8, 'Essence_latin': 9, 'Essence_fr': 10, 'Essence_ang': 11, 'DHP,Date_Releve': 12, 'Date_Plantation': 13, 'LOCALISATION': 14, 'Localisation_code': 15, 'CODE_PARC': 16, 'NOM_PARC': 17, 'Rue_de': 18, 'Rue_a': 19, 'Distance_pave': 20, 'Distance_ligne_rue': 21, 'Stationnement_jour': 22, 'Stationnement_heure': 23, 'District': 24, 'Arbre_remarquable': 25, 'Code_secteur': 26, 'Nom_secteur': 27, 'Coord_X': 28, 'Coord_Y': 29, 'Longitude': 30, 'Latitude:': 31,}## For debugging and tracing purposes#def output(txt): print("\n*** " + datetime.now().isoformat() + " ***") pprint.pprint(txt)## Lecture du CSV#output("Lecture de la base de données d'arbres")trees_csv = ()with open(TREES_CSV, mode='r') as file: # Create a CSV reader object trees_csv = list(csv.reader(file)) # Remove first element, it's the header trees_csv.pop(0)#output(trees_csv)# The names of the dictionary keysTOTAL = 'Total'ESSENCE = 'Essence'SIGLE = 'Sigle'# The maximum number of trees to displayMAX_TREES = 15## Grouping by Tree type (Sigle)## A dictionary of dictionaries. Key of outer dict is the type (Sigle), inner dict is made of the# name of the tree (Essence) and the total number in Montreal for that particular treetrees_by_type = {}for row in trees_csv: # Let's get the type (Sigle) of the tree in the this row sigle = row[COL[SIGLE]] # If we already have this type of tree in our dict, incretment the total number if sigle in trees_by_type: trees_by_type[sigle][TOTAL] += 1 # otherwise create that key, and get the name of the tree (Essence) and set the total to 1 else: trees_by_type[sigle] = {TOTAL: 1, ESSENCE: row[COL['Essence_fr']]}# Sort by 'total' in descending ordersorted_trees = sorted(trees_by_type.values(), key=lambda x: x[TOTAL], reverse=True)[:MAX_TREES]output(sorted_trees)## Time to generate the scene# # Select all objects in the scenebpy.ops.object.select_all(action='SELECT')output("Cleaning up the scene")# Delete all selected objectsbpy.ops.object.delete()output("Purging unused data if any")# Clear out any meshes, materials, etc. that might still be in memorybpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)# Bar propertiesbar_height = 1.0 # Fixed height (Y axis)bar_depth = 0.5 # Fixed depth (Z axis)spacing = 2.2 # Space between bars on the Y axisscale=10# Find max and min TOTAL to normalize the datamax_total = max(item[TOTAL] for item in sorted_trees)min_total = 0# Normalize TOTAL values (scaling between 0 and 1)for item in sorted_trees: item['TOTAL_NORM'] = (item[TOTAL] - min_total) / (max_total - min_total) if max_total > min_total else 1# Create an empty, we will use it for some animationbpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))empty = bpy.context.object# Loop through data and create barsfor i, entry in enumerate(sorted_trees): total = entry[TOTAL] total_norm = entry['TOTAL_NORM'] # Use normalized value for the length of the bars essence = entry[ESSENCE] # Create the bar (starting at X = 0, so no need to move it along the X axis) bpy.ops.mesh.primitive_cube_add(location=(0, -i * spacing, 0)) # Adjust location so the bar starts at X=0 bar = bpy.context.object bar.scale = (total_norm * scale, bar_height, bar_depth) # Multiply by a scaling factor to visualize better bar.location.x = total_norm * scale # Make the bar the child of emtpy bar.parent = empty # Create a material for the bar based on the TOTAL value (color from red to green) mat = bpy.data.materials.new(name=f"BarMaterial_{i}") color_intensity = total_norm # Use normalized TOTAL for color intensity mat.diffuse_color = (1 - color_intensity, color_intensity, 0, 1) # Transition from red to green bar.data.materials.append(mat) # Create text for essence bpy.ops.object.text_add(location=(-0.5, -i * spacing, 0)) # Place the text to the left of the bar text_obj = bpy.context.object text_obj.data.body = essence text_obj.data.align_x = 'RIGHT' # Make the text the child of emtpy text_obj.parent = empty # Create text for the total label bpy.ops.object.text_add(location=(0.5, -i * spacing - spacing / 4, bar_height/2)) # Place the text to the right of the bar text_obj = bpy.context.object text_obj.data.body = str(total) text_obj.data.align_x = 'LEFT' text_obj.scale = (0.5, 0.5, 0.5) bpy.ops.transform.translate(value=(total_norm * scale * 2, 0, 0), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(True, False, False), mirror=False, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False, snap=False, snap_elements={'INCREMENT'}, use_snap_project=False, snap_target='CLOSEST', use_snap_self=True, use_snap_edit=True, use_snap_nonedit=True, use_snap_selectable=False) # Make the label the child of emtpy text_obj.parent = empty# Set up the cameracam_data = bpy.data.cameras.new(name="Camera")cam_obj = bpy.data.objects.new("Camera", cam_data)bpy.context.collection.objects.link(cam_obj)# Set camera rotation and lensecam_obj.rotation_euler = (0, 12 * math.pi/180, 0) # Rotate to face the graph (along -Z axis)cam_obj.data.lens = 35cam_obj.location = (scale + MAX_TREES, -MAX_TREES, MAX_TREES * scale/1.5) # Adjust camera position based on the largest bar# Make the camera activebpy.context.scene.camera = cam_obj# Set up lightinglight_data = bpy.data.lights.new(name="Light", type='AREA')light = bpy.data.objects.new(name="Light", object_data=light_data)light.data.energy = 500light.location = (10, -MAX_TREES, 10)bpy.context.collection.objects.link(light)## Time for some animation!#start_frame = 1end_frame = 120min_rotation = -30max_rotation = 30bpy.context.scene.frame_start = start_framebpy.context.scene.frame_end = end_framedef setKeyframe(object, frame, rotation): bpy.context.scene.frame_set(frame) # Set current frame object.rotation_euler[1] = math.radians(rotation) # Rotate along Y-axis object.keyframe_insert(data_path="rotation_euler", index=1) # Insert keyframe for Y-axissetKeyframe(empty, start_frame, min_rotation)setKeyframe(empty, int(round(end_frame - start_frame)/2), max_rotation)setKeyframe(empty, end_frame, min_rotation)# Set interpolation to linearfor fcurve in empty.animation_data.action.fcurves: for keyframe in fcurve.keyframe_points: keyframe.interpolation = 'BEZIER' # Set interpolation to linearbpy.context.scene.frame_set(1)# Play the animationbpy.ops.screen.animation_play()
Un énorme merci à Thijs Manders et ses tutos sur sa chaîne MTR Animation, avec son aimable permission!
Démarrer Blender, la version la plus récente
Créer les geo nodes suivants. Pour le noeud Import CSV, faites-le pointer vers le fichier CSV que vous venez de télécharger.
On va. faire un peu de math. On veut les points distribués sur une sphère.
Pour se faire, voici les formules mathématiques de projection sphérique: x=cos(Longitude)∗cos(Latitude)∗c y=sin(Longitude)∗cos(Latitude)∗c z=sin(Latitude)∗c
Alors donc allons-y!
On va faire la première formule. Donc deux maths nodes de type cosine.
☝🏼
Essayez ensuite ctrl-shift-rmb et drag entre les deux noeuds Cosine. Blender va ajouter un math node qui combine les deux, on doit juste changer add pour multiply.
Note: Il faut activer l’addon “Node Wrangler”.
On doit également multiplier le résultat par une constante (c). Décidons pour l’instant d’une valeur de c = 4. On changera ça plus tard si nécessaire.
Ça c’est pour l’axe des X, donc on connecte le dernier multiply au socket X du Combine XYZ.
On doit faire la même chose pour y et z, sauf que y c’est sin * cos et z juste sin. Changeons également Value pour 50.
Bon, on voit un globe, mais y’a pas de continents. La raison en est que Longitude et Latitude sont données en degrés dans le CSV alors que Blender s’attend à des valeurs en radian.
On va créer un reroute au niveau des deux sorties de Longitude et des 3 sorties de Latitude.
☝🏼
Un truc pour ça: Shift-rmb et drag à travers les noodles!
Woah, c’est-y pas beau tout ça!
Maintenant, on va montrer la population par ville, on a cette donnée!
Commençons simplement, avec le set point radius. Mais faut remapper (map range!).
Mais on ne sait pas à l’avance le min et le max de la colonne Population. Comment faire?
Le noeud Attribute Statistics is the key!
Donc le map range va distribuer les valeurs qui sont, à l’origine entre Min et Max de la valeur Population, à un intervalle de [0.02, 0.7] par exemple.
Zoomez et allez voir vos villes préférées! Voyez par exemple la région montagneuses (Les rocheuses, les Alpes, l’Himalaya, y’a moins de points ou de très petits points!
Remarquez que ça coûte très peu cher car ce n’est pas de la géométrie encore!
Dataviz Jam I - EULA - Suite et fin!
Continuer à bosser sur votre dataviz. C’est à remettre au prochain cours.
# Cours 6
Banque de cours - Major cities population mapped onto Earth - Partie 2
Major cities population mapped onto Earth - Partie 2
On va créer la Terre maintenant.
Ajouter une UV Sphere (une vraie), scale de manière à ce qu’elle soit juste un peu plus petite que les points en terme de diamètre.
Ajouter un subdivision surface modifier (2 pour le viewport, 3 pour le render).
Cachons les points (sélectionnez l’objet dont vous vous êtes servi pour créer le geo nodes et appuyer sur H.
Avec la sphère sélectionnée, allons dans le Shader Editor, Object Shader, et faire New Material que vous nommerez Earth.
Créez un Image Texture Node, allez chercher l’image “8k_earth_specular_map.tif” et connectez-la au socket Base Color du Principled BSDF.
On va créer un outline en se basant sur le fait qu’il y a gradiant entre le blanc (l’eau) et le noir (les continents).
Avec un Math node, on va aller chercher les valeurs entre 0.1 et 0.3 (donc le gris à la frontière), on les multiplie ensemble, ça va donner la valeur 0.03 et c’est cette valeur qu’on va utiliser de la texture.
Ça fait un super contour:
On va ajuster la puissance des lignes de contour:
Vous avez deviner, c’est en contrôlant la “Strength” de l’Emission Color qu’on peut arriver à le faire.
Je vais choisir un ton bleuté
Aussi, right-click sur la sphère et faire “Shade Smooth”
Aussi, je vais rendre notre sphère métalique et augmenter la roughness
Qu’est-ce qu’elle a la Terre de particulier que la Lune n’a pas? Une atmosphère bien sûr. On va en ajouter une.
Pour ce faire, un Layer Weight, qui offre un type de lentille fresnel va nous aider.
☝🏼
Les noeuds Fresnel et Layer Weight:
Le Fresnel Node et le Layer Weight Node sont essentiels pour créer des matériaux réalistes dans Blender, surtout lorsqu’on veut simuler les reflets, la translucidité ou le comportement optique des surfaces selon l’angle de vue.
1. Principe général : l’effet Fresnel
Le Fresnel (d’après Augustin Fresnel) décrit le phénomène selon lequel une surface réfléchit plus fortement à ras du regard (grazing angle) qu’en face directe.
➡️ En d’autres termes :
Quand tu regardes une surface de face, elle réfléchit peu.
Quand tu la regardes de biais, elle réfléchit davantage.
Ce principe est crucial pour tous les matériaux physiques (verre, eau, métal, plastique, etc.).
2. Le Fresnel Node
🔹 Localisation :
Shader Editor → Add → Input → Fresnel
🔹 Fonction :
Ce nœud calcule le facteur Fresnel pur à partir de l’indice de réfraction (IOR) du matériau.
La sortie du nœud donne une valeur entre 0 et 1 représentant la proportion de lumière réfléchie selon l’angle de vue.
🔹 Entrée principale :
IOR (Index of Refraction) : contrôle la courbe du dégradé.
Verre ≈ 1.45
Eau ≈ 1.33
Plastique ≈ 1.45
Métal (non applicable directement, car les métaux ont une réflexion complexe)
🔹 Sortie :
Fac (Factor) : intensité du Fresnel à chaque pixel.
→ 0 = vue de face, 1 = vue de biais.
→ Bord du modèle illuminé, centre plus sombre : effet de halo stylisé (très utilisé pour les effets toon ou outlines).
Astuce bonus :
Tu peux inverser la sortie “Facing” avec un ColorRamp inversé pour avoir le comportement opposé (par ex. éclairer le centre au lieu des bords).
Ce serait chouette de voir les longitudes et les latitudes. Il y a un moyen simple avec les geo nodes.
Ajouter un petit workspace et choisir Geometry nodes.
Avec la sphère sélectionné, faites New Geo Nodes.
Ajoutez un noeud de type Mesh to Curve et choisir Edges plutôt que Faces.
Mais on ne voit plus la terre. On va faire un join geometry. Rappelez-vous pouvez faire ctrl-shift et right-lmb et drag Le Group Input et le Mesh to Curve ça va les “additionner” de manière intelligente (ici, c’est bien sûr un join geometry).
Aussi, je trouve qu’il y’en a trop, je vais modifier le subdivision surface à 1 pour Viewport et Render:
Ceci dit, je ne veux pas si la grille sur les continents, seulement les océans.
Comment faire?
On peut utiliser la même texture de la map terrestre et s’en servir comme masque de sélection sur un noeud Delete Geometry!
Pas tout à fait ça encore, et c’est parce que la texture n’est pas mappé selon les UV. Pour ça, on va aller chercher les UV et utiliser un Maprange pour inverser le masque afin que la grille soit sur l’océan seulement:
☝🏼
On aurait pu uitiliser un Mathy Boolean de type “Not” plutôt qu’un Map Range, c’est même plus logique
Maintenant faut travailler les frontières. Vous remarquez, en mode Render, les edges sont effacés au complet ou rien. On veut donc plus de points par edge. Un resample curve va aider.
Bon, maintenant, ça traverse un peu sur les continents, y’a beaucoup de lignes semi effacées, et ça c’est dû au fait que Blender a de la difficulté à calculer sur des curves. Delete geometry, ça marche bien sur de la geometry, par sur des curves. On va convertir en mesh et ça va marcher.
Aaaah, beaucoup plus mieux!
☝🏼
Attention, dans Blender 5.x, l’ordre du join geometry est important ! Si vous ne voyez plus les continents, assurez-vous que l’entrée socket Geometry du Group Input est avant (plus haut) celle du Delete Geometry.
Ajustez la valeur de count dans Resample Curve pour améliorer encore plus la précision.
Un peu d’épaisseur pour longitudes et latitudes? Très facile vu que Curve to Mesh a un socket Profile Curve!
Un cercle comme profile curve, ça vous dit?
Un material sur les lignes serait sympa.
Dans la partie Shader (à gauche), cliquez sur la flèche de Slot et cliquer sur + pour créer un nouveau slot.
Ensuite cliquer sur New Material et nommez-le “Grid”.
Effacer le Principled BSDF et créer un noeud Emission.
Couleur dun ton bleuté
Strength = 6.
Du côté du geo nodes, insérer un Set Material entre Delete Geometry et Join Geometry (On veut cette texture seulement sur les lignes qui surplomblent les océeans).
Choisir Grid comme Material du noeud Set Material.
Dans le World Properties, mettre le strength de l’environment map à 0.
☝🏼
Si votre ordi en arrache avec le “Updating lights”, allez dans Render properties, sous Light et désactivez Light Tree.
On continue à travailler sur notre devoir Dataviz EULA
# Cours 7
Banque de cours - Major cities population mapped onto Earth - Partie 3
Major cities population mapped onto Earth - Partie 3
Maintenant, ramenons les points (L’oeil à droite de l’objet Plane dans l’Outliner).
On aimerait les rendre plus visibles.
Avec les points sélectionnés, faites “New” do côté du Shader Editor.
Appelez ce Material “Cities”.
Remplacer Principled BSDF par un Emission Shader.
Ajoutez un Set Material dans la section Geo nodes.
Du côté de Emission, mettez la valeur de Strength à 20.
On veut maintenant que la couleur des points dépendent de la population à cet endroit.
On veut utiliser le même principe dans le Shader Editor que dans le Geo nodes: la population passant à travers un Map Range pour réduire l’échelle sans changer le rapport de proportions.
Cela étant dit, il n’y a pas de noeuds Map Range dans le Shader Editor (pas encore). On va donc créer des variables (Store Named Attributes) dans les Geometry Nodes qu’on va ensuite utiliser (Named Attribute) dans le Shader Editor.
On va donc utiliser le map range, mais un nouveau parce qu’on veut une valeur variant entre 0 (aucune couleur) et 1 (pleine saturation).
Un color ramp va rendre ça plus beau. Il faut par contre amener la couleur du côté très grandes populations vers la gauche pour qu’en en voit plus.
Il faut également réduire le diamètre des points, ça, c’est la valeur “To Min” du premier Map Range, on va le diviser par 10: de 0.02, on va passer à 0.002.
Maintenant qu’on connaît bien les lentilles Fresnel, ajoutons un petit glow autour des points.
À l’aide d’un noeud Layer Weight pour contrôler le Strength de l’Emission Shader.
Les points sont trop distancés par rapport à la sphère, corrigeons le tir.
Pour ce faire, on va positionner chaque point (Set position) sur la surface la plus proche (Geometry Proximity) de la sphère.
On va faire un drag and drop de la sphère dans le geo node pour obtenir son Object Info et on va s’en servir pour alimenter un noeud Set Position.
Vous remarquez que les points sont décalés un petit peu par rapport aux frontières. On peut ajuster en simplement faisant une rotation de la sphère selon son axe local (R R).
☝🏼
Attention, c’est lourd, allez-y doucement avec la touche Shift pour des micro-rotations.
Après ajustement:
Ça ne sera pas parfait, mais l’Univers s’en fout.
Vous pouvez aussi jouer avec la constante C (50) et l’échelle de la sphère (50,50,50), mais, normalement, ils devraient avoir la même valeur.
Le volume ambiant
Créer un cube beaucoup plus grand que la sphère (scale de 100 par exemple).
Dans data, Viewport Display, choisissez Wireframe.
Aussi, allez en mode Material Preview. Comme le lighting n’est pas encore fait, la sphère va apparaître noire en Render Mode.
Avec le cube sélectionné, allez dans le Shader Editor.
Vous pouvez phagociter le Geo Nodes workspace
On va créer un nouveau Shader (New), et on le nomme “Space Volume”.
Effacer le Principled BSDF et le remplacer par un Principled Volume.
Attention, il doit être connecté à Volume et non pas Surface du Material Output.
On va contrôler la couleur et la densité avec des noise textures disposées en fonction des texture coordinates de l’objet.
Pour la couleur, on va rendre la distribution des points entre 0 et 1 plus “smooth” (Map Range avec Smoother Step) avec un Map Range où on ne change pas les from et To min max.
Il faut aussi faire passer ce noise par un noeud Hue/Saturation/Value, on veut contrôler juste le Hue (la teinte) et avoir une valeur très faible (0.1).
La couleur, on va la choisir un peut bleutée.
Pour la densité, un Map Range qui est très petit à la sortie (entre 0 et 0.01). Voici des valeurs qui fonctionnent bien:
Oupsie, vous voyez l’erreur? J’ai branché le socket Density Attribute. C’est densité que je dois brancher.
J’ai aussi utiliser le socket Color (3 valeurs) des Noise Texture alors que Map Range s’attend à une seule valeur. Il faut utiliser le socket “Fac” à la place:
Revenir en mode Layout et en mode Rendered.
Ajouter une Light de type Sun et ajuster sa puissance à 10.
Les étoiles
Ça se fait très bien avec une IcoSphere.
Ajoutons donc une Ico Sphere avec un scale de 130 environ.
Right-Click et faire Shade Smooth.
Dans Viewport Display de la section Data Properties, Display as Wire comme le cube.
Ceci dit, on ne veut pas que le soleil affecte l’icosphere (c’est pour ça qu’elle apparaît blanche en haut).
Sélectionner le cube, faire “M” pour New Collection et nommer la collection “Fog”.
Ensuite, sélectionner l’objet Sun et dans les propriétés Data:
Aller dans la section Shading et choisir Light Linking.
Choisir la collection “Fog”.
Maintenant, le soleil n’éclaire que le volume du cube:
Pour les étoiles, sélectionnez l’Icosphere et aller dans le Shader Editor.
On va utiliser un Voronoi pour ça.
Faire New et nommer le Material Stars.
On va contrôler les étoiles et leur intensité avec l’alpha du Principled BSDF (on garde une couleur blanche) et le strength de l’Emission de ce dernier:
En zoomant un peu, je trouve que les grosses étoiles (threshold de 0.1) sont trop grosses. Je vais baisser le seuil (Threshold) à 0.05:
La beauté des cellules Voronoi, qu’on retrouve partout dans la nature
Un diagramme de Voronoï (ou polygones de Thiessen) est une partition d’un espace en régions adjacentes (appelées cellules) à partir d’un ensemble de points appelés “germes”
Chaque cellule contient un seul germe, et tous les points de cette cellule sont plus proches du germe qui s’y trouve que de tout autre germe.
La frontière entre une cellule et une autre est un segment de droite qui se trouve à égale distance entre le germe de la première cellule et celui de la deuxième.
Pavage de l’espace : Il divise l’espace (un plan, par exemple) en un ensemble de polygones.
Cellules d’influence : Chaque cellule délimite la zone la plus proche d’un point spécifique par rapport à tous les autres points de l’ensemble.
Applications : Ils sont utilisés dans de nombreux domaines, comme l’analyse spatiale (délimitation de zones d’influence) ou la géométrie.
Tout compte fait, un scale de 200 au lieu de 500 pour le Voronoi donne un meilleur résultat:
Compositing
Ajoutez une caméra
Passez en mode Caméra (cliquez sur l’icône caméra près du N menu dans le viewport ou faites 0 sur le numpad).
Cliquez sur le cadenas près du N Menu dans le viewport (juste en dessous de la caméra).
Ça va activer le “View navigation within the Camera View”
Maintenant faire Ctrl-B et créez une rectangle qui définira le frame de la caméra.
Cette technique permet de cacher ce qui sort du frame de la caméra.
Vu qu’on est en mode View Lock, utilisez la souris pour zoomer in/out, panner et tourner pour choisir le bon point de vue.
Pour mieux voir les étoiles, dans les propriétés de la caméra, changez le clip End à 1000 m par exemple.
Dans les Render Properties, choisir Cycles et changer le Max Samples à 128
Allez dans Render et faire Render Image pour avoir au moins un Frame et ainsi pouvoir voir l’effet du compositing.
Aller en Mode Compositing.
Pour ajuster le zoom de l’image, choisir View du côté du N Menu et ajuster le paramètre Zoom.
Commençons avec les noeuds de base suivants (vous vous souvenez? Celui du fichier Default.blend):
☝🏼
Dans Options du N Menu du Compositor, choisissez GPU pour accélérer le processus de compositing
- [ ] La couleur du Lift du Color Balance, un peu bleutée
- [ ] Un Glare de type Fog Glow pas trop fort
- [ ] Une légère distortion de la lentille
- [ ] Un peu plus de contraste et de brightness
- [ ] Un gamma légèrement augmenté
Ajoutez un effet de vignette:
Pour ce faire, on ajoute un masque de type Ellipse.
La taille est fonction de la taille de l’image (ici 40% de l’image)
On fait passer tout ça dans un Gaussian Blur.
Que l’on combine avec l’image elle-même à l’aide du mode Overlay. Aussi, j’ai désactivé (M pour Mute), le noeud Lens Distortion, il enlève trop des étoiles.
Il est important ici de désactiver le denoise, car ce dernier va retirer la majorité des étoiles, qu’il interprètera comme du bruit!
Conséquemment, faut augmenter le nombre d’échantillons et réduire le noise threshold.
J’ai ajusté le Voronoi des étoiles, je trouvais qu’il y’avait trop de gros points blancs:
Voici donc le résultat:
Je vous avoue que moi, je préfère sans compositing du tout. À vous de jouer!
Mon tout premier projet professionnel 3D - Parcours 3D dans un labyrinthe, commande par la voix
/*========================================================================*\|| |||| MODULE : MAZE.C |||| |||| |||| DESCRIPTION: Programme qui genere un labyrinthe 2-D a l'aide de |||| l'algorithme du "spanning tree" (arbre de recouvrement). |||| |||| REMARQUES : L'impression est en mode texte. |||| |||| |||| AUTEUR : Karim Aktouf |||| |||| CREATION : le 10 aout 1993 <-- VOUS N'ÉTIEZ PAS NÉS! |||| |||| REVISION : le 13 aout 1993 |||| le 03 juin 1996 - Les parametres ont maintenant une |||| valeur par defaut, et ajout de deux |||| autres parametres pour specifier le |||| charactere pour les lignes horizontales |||| et celui des lignes verticales. |||| ||\*========================================================================*/#include <stdio.h>#include <stdlib.h>#include <time.h>typedef enum _bool {FALSE, TRUE} BOOL;typedef unsigned TypedeCell;#define AUCUN 0#define SPAN 1#define FRONTIERE 2typedef unsigned TypedeCase;#define FERMEE 0#define OUVERTE 1/* La norme ANSI veut pas de enum dans des bit fields !!! * Dommage, c'est elegant... * * typedef enum _TypedeCell {AUCUN, SPAN, FRONTIERE} TypedeCell; * typedef enum _TypedeCase {FERMEE, OUVERTE} TypedeCase; */typedef struct _Cell Cell;typedef struct _FCell FCell;struct _Cell{ TypedeCell Type:2; TypedeCase Bas:1, Droite:1;};struct _FCell /* Element de la liste circulaire de cellules */{ /* frontiere */ int L; int C; FCell *Next; FCell *Prev;};/*========================================================================*\|| |||| Prototypes des fonctions utilisees |||| ||\*========================================================================*/Cell **InitMaze (void);void GenerateMaze (int L, int C);void PrintMaze (char H, char V);void AddToFList (FCell *FPtr);void RemoveFromFList (FCell *FPtr);void MarkAdj (int L, int C, TypedeCell T);FCell *ChooseRandomF (void);void ConnectToSPAN (int L, int C);/*========================================================================*\|| |||| Variables globables |||| ||\*========================================================================*/Cell **MazePtr = NULL;FCell *FList = NULL;int NbL=10, /* Nombre de lignes */ NbC=10, /* Nombre de colonnes */ NbF=0; /* Nombre d'elements dans FList */char H='|', /* Char. des lignes horizontales */ V='|'; /* Char. des lignes verticales */int main(int argc, char **argv){ time_t t; int i=0; if (argc >= 2) if ((NbL = atoi(argv[1])) < 2) { printf("\nNombre de lignes invalide (%d) doit etre au moins 2.\n", NbL); return -1; } if (argc >= 3) if ((NbC = atoi(argv[2])) < 2) { printf("\nNombre de colonnes (%d) doit etre au moins 2.\n", NbC); return -1; } if (argc >= 4) H=*argv[3]; if (argc >= 5) V=*argv[4]; if (!(MazePtr = InitMaze())) return -1; /* On choisit une cellule au hasard entre i=1..NbL-1 et j=1..NbC-1 */ srand((unsigned) time(&t)); GenerateMaze((rand() % (NbL-1)) + 1, (rand() % (NbC-1)) + 1); PrintMaze(H, V); return 0;}/*========================================================================*\|| |||| Fonction : InitMaze |||| |||| Description: Alloue l'espace memoire necessaire et initialise aux |||| valeurs par defaut chaque cellule du labyrinthe. |||| |||| Parametres : nil |||| |||| Retour : Pointeur a l'espace memoire occupe par le labyrinthe |||| |||| Remarques : Le labyrinthe est organise en matrice 2-D dynamique, |||| c'est-a-dire que le pointeur pointe a une COLONNE de |||| pointeurs contenant NbL pointeurs, chacuns pointant sur |||| une LIGNE de NbC elements. l'acces a la cellule de la |||| LIGNE i, COLONNE J se fait alors par [i][j] tout |||| simplement, comme s'il s'agissait d'un tableau 2-D |||| statique. |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 10 aout 1993 |||| |||| Revision : le 13 aout 1993 |||| ||\*========================================================================*/Cell **InitMaze(void){ Cell **Maze; int i, j; if (!(Maze = (Cell **) malloc(sizeof(Cell *) * NbL))) return NULL; for (i=0; i < NbL; i++) { if (!(Maze[i] = (Cell *) malloc(sizeof(Cell) * NbC))) return NULL; for (j=0; j < NbC; j++) { Maze[i][j].Type = AUCUN; Maze[i][j].Bas = FERMEE; Maze[i][j].Droite = FERMEE; } } /* Le mur du haut est un peu special */ for (j=0; j < NbC; j++) Maze[0][j].Droite = OUVERTE; /* Meme chose pour le mur de droite */ for (i=0; i < NbL; i++) Maze[i][0].Bas = OUVERTE; return Maze;} /* InitMaze *//*========================================================================*\|| |||| Fonction : GenerateMaze |||| |||| Description: Genere le labyrinthe, en partant de la cellule L,C |||| choisie prealablement au hasard. |||| |||| Parametres : L : Ligne de la cellule choisie au hasard |||| C : Colonne de la cellule choisie au hasard. |||| |||| Retour : nil |||| |||| Remarques : Pour ne pas generer a chaque fois le meme labyrinthe |||| avec les meme parametres, il faut s'assurer |||| d'initialiser le generateur de nombre aleatoires a une |||| valeur differente a chaque fois. Pour ce faire, il faut|||| se baser sur la valeur de l'horloge de l'ordinateur, qui|||| varie tout le temps: |||| |||| |||| #include <time.h> |||| #include <stdio.h> |||| |||| void main(void) |||| { |||| time_t t; int i; |||| |||| srand ((unsigned) time(&t)); |||| |||| for (i=0; i < 100; i++) |||| printf("%d ",rand() % 100); |||| } |||| |||| |||| Ce bout de code generera 100 nombres aleatoires |||| differents a chaque appel du programme. Si on omet le |||| srand(nombre associe a l'horloge), la meme serie de |||| nombres pseudo-aleatoires sera generee a chaque fois. |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 10 aout 1993 |||| |||| Revision : le 13 aout 1993 |||| ||\*========================================================================*/void GenerateMaze(int L, int C){ FCell *FPtr; int LL, CC; /* On marque la cellule choisie au hasard comme etant SPAN */ MazePtr[L][C].Type = SPAN; /* On marque toutes les cellules adjacentes comme etant FRONTIERE */ MarkAdj(L, C, FRONTIERE); while (FList) /* Tant que la liste des cellules frontieres n'est pas vide */ { /* On choisit une cellule frontiere au hasard */ FPtr = ChooseRandomF(); /* On la marque comme etant SPAN */ LL = FPtr->L; CC = FPtr->C; RemoveFromFList(FPtr); MazePtr[LL][CC].Type = SPAN; /* On la connecte a une cellule SPAN adjacente prise au hasard */ ConnectToSPAN(LL, CC); /* On marque les cellules adjacentes de type AUCUN a FRONTIERE */ MarkAdj(LL, CC, FRONTIERE); } /* On choisit l'entree... */ MazePtr[1][0].Droite = OUVERTE; /* et la sortie. */ MazePtr[NbL-1][NbC-1].Droite = OUVERTE;} /* GenerateMaze *//*========================================================================*\|| |||| Fonction : ChooseRandomF |||| |||| Description: Choisit au hasard une cellule frontiere parmis la liste |||| de cellules frontieres. |||| |||| Parametres : nil |||| |||| Retour : Pointeur a la cellule choisie. |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 10 aout 1993 |||| |||| Revision : le 11 aout 1993 |||| ||\*========================================================================*/FCell *ChooseRandomF(void){ int tmp = (rand() % NbF) + 1; int i=1; FCell *FPtr = FList; while (i++ != tmp) FPtr = FPtr->Next; return FPtr;} /* ChooseRandomF *//*========================================================================*\|| |||| Fonction : ConnectToSPAN |||| |||| |||| Description: Connecte la cellule L,C a une cellule SPAN adjacente, |||| prise au hasard. |||| |||| Parametres : L : Ligne de la cellule a connecter |||| C : Colonne de la cellule a connecter |||| |||| Retour : nil |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 12 aout 1993 |||| |||| Revision : le 13 aout 1993 |||| ||\*========================================================================*/void ConnectToSPAN(int L, int C){ BOOL Span_Adj[4] = {FALSE}; int NbSpanAdj = 0; int tmp, i, j, SpanToConnect; /* cellule adjacente du haut */ if ((L-1 > 0) && (MazePtr[L-1][C].Type == SPAN)) { Span_Adj[0] = TRUE; NbSpanAdj++; } /* cellule adjacente de droite */ if ((C+1 < NbC) && (MazePtr[L][C+1].Type == SPAN)) { Span_Adj[1] = TRUE; NbSpanAdj++; } /* cellule adjacente du bas */ if ((L+1 < NbL) && (MazePtr[L+1][C].Type == SPAN)) { Span_Adj[2] = TRUE; NbSpanAdj++; } /* cellule adjacente de gauche */ if ((C-1 > 0) && (MazePtr[L][C-1].Type == SPAN)) { Span_Adj[3] = TRUE; NbSpanAdj++; } if (NbSpanAdj < 1) /* Ne devrais jamais arriver!!! */ { printf("\nWow!!! Pas de cellule SPAN adjacente a Cell(%d, %d)!!!!\n", L, C); exit(-1); } if (NbSpanAdj == 1) { tmp=-1; while(Span_Adj[++tmp] == FALSE) ; SpanToConnect = tmp; } else { tmp = (rand() % NbSpanAdj) + 1; i=0; for (j=0; j < 4; j++) { if (Span_Adj[j] == TRUE) { if (++i == tmp) { SpanToConnect = j; break; } } } } /* Ok, La cellule adjacente numero SpanToConnect est la SPAN a connecter */ switch(SpanToConnect) { case 0: MazePtr[L-1][C].Bas = OUVERTE; break; case 1: MazePtr[L][C].Droite = OUVERTE; break; case 2: MazePtr[L][C].Bas = OUVERTE; break; case 3: MazePtr[L][C-1].Droite = OUVERTE; break; }} /* ConnectToSPAN *//*========================================================================*\|| |||| Fonction : MarkAdj |||| |||| Description: Marque comme etant de type FRONTIERE toutes les cellules|||| de type AUCUN voisines de la cellule L,C |||| |||| Parametres : L : Ligne de la cellule dont on doit marquer les |||| cellules adjacentes. |||| C : Colonne de la cellule dont on doit marquer les |||| cellules adjacentes. |||| |||| Retour : nil |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 11 aout 1993 |||| |||| Revision : le 11 aout 1993 |||| ||\*========================================================================*/void MarkAdj(int L, int C, TypedeCell T){ FCell *FPtr; /* Cellule adjacente du haut */ if (L-1 > 0) { if (MazePtr[L-1][C].Type == AUCUN) { MazePtr[L-1][C].Type = T; if (!(FPtr = (FCell *) malloc(sizeof(FCell)))) { printf("\nPas assez de memoire pour FPtr\n"); exit(-1); } FPtr->L = L-1; FPtr->C = C; AddToFList(FPtr); } } /* Cellule adjacente du bas */ if (L+1 < NbL) { if (MazePtr[L+1][C].Type == AUCUN) { MazePtr[L+1][C].Type = T; if (!(FPtr = (FCell *) malloc(sizeof(FCell)))) { printf("\nPas assez de memoire pour FPtr\n"); exit(-1); } FPtr->L = L+1; FPtr->C = C; AddToFList(FPtr); } } /* Cellule adjacente de gauche */ if (C-1 > 0) { if (MazePtr[L][C-1].Type == AUCUN) { MazePtr[L][C-1].Type = T; if (!(FPtr = (FCell *) malloc(sizeof(FCell)))) { printf("\nPas assez de memoire pour FPtr\n"); exit(-1); } FPtr->L = L; FPtr->C = C-1; AddToFList(FPtr); } } /* Cellule adjacente de droite */ if (C+1 < NbC) { if (MazePtr[L][C+1].Type == AUCUN) { MazePtr[L][C+1].Type = T; if (!(FPtr = (FCell *) malloc(sizeof(FCell)))) { printf("\nPas assez de memoire pour FPtr\n"); exit(-1); } FPtr->L = L; FPtr->C = C+1; AddToFList(FPtr); } }} /* MarkAdj *//*========================================================================*\|| |||| Fonction : AddToFList |||| |||| Description: Ajoute l'element pointe par FPtr dans la liste |||| circulaire a double liens des cellules de type FRONTIERE|||| |||| Parametres : FPtr : Pointeur a l'element a ajouter. |||| |||| Retour : nil |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 10 aout 1993 |||| |||| Revision : le 10 aout 1993 |||| ||\*========================================================================*/void AddToFList(FCell *FPtr){ if (!FList) { FList = FPtr; FList->Next = FList; FList->Prev = FList; } else { FList->Prev->Next = FPtr; FPtr->Next = FList; FPtr->Prev = FList->Prev; FList->Prev = FPtr; } NbF++;} /* AddToFList *//*========================================================================*\|| |||| Fonction : RemoveFromFList |||| |||| Description: retire de la liste circulaire l'element pointe par FPtr.|||| |||| Parametres : FPtr : Pointeur a l'element a retirer. |||| |||| Retour : nil |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 10 aout 1993 |||| |||| Revision : le 10 aout 1993 |||| ||\*========================================================================*/void RemoveFromFList(FCell *FPtr){ if (FList->Next == FList) { free(FList); FList = NULL; } else { if (FList == FPtr) FList = FPtr->Next; FPtr->Prev->Next = FPtr->Next; FPtr->Next->Prev = FPtr->Prev; free(FPtr); } NbF--;} /* RemoveFromFList *//*========================================================================*\|| |||| Fonction : PrintMaze |||| |||| Description: Imprime le labyrinthe en mode texte. Chaque cellule |||| est representee par: |||| |||| Case toujours ouverte-> HV <- Case droite, OUVERTE, FERMEE |||| |||| Case Bas, OUVERTE, -> VH HV <- Case toujours fermee |||| FERMEE |||| |||| Parametres : nil |||| |||| Retour : nil |||| |||| Remarques : nil |||| |||| Auteur : Karim Aktouf |||| |||| Creation : le 12 aout 1993 |||| |||| Revision : le 12 aout 1993 |||| ||\*========================================================================*/void PrintMaze(char H, char V){ int i, j; printf("\n\nMaze %dx%d:", NbL, NbC); for (i=0; i < NbL; i++) { printf("\n"); for (j=0; j < NbC; j++) if (MazePtr[i][j].Droite == OUVERTE) printf(" "); else printf(" %c%c", H, V); printf("\n"); for (j=0; j < NbC; j++) if (MazePtr[i][j].Bas == OUVERTE) printf(" %c%c", H, V); else printf("%c%c%c%c", V, H, H, V); } printf("\n");} /* PrintMaze */
Générateur de Labyrinthes
Un peu de théorie
Here's some info I wrote up once for a C class assignment. I can send you C
code if you want, but your request asked for the algorithm...
Dr. Rick Perry, ECE Department, Villanova University, Villanova, PA 19085
215-645-4969, fax: 215-645-4436, hm: 215-259-8734
Creating a maze
Here is an example of a maze of size 5 by 8:
0 1 2 3 4 5 6 7 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 0 XX XX XX XX XX XX XXXXXX XXXXXX XXXXXX XX 1 XX XX XX XX XX XXXXXXXXXXXXXX XXXXXXXXXX XX 2 XX XX XX XX XX XXXXXX XXXXXX XX XX XX XX 3 XX XX XX XX XX XX XX XXXXXX XXXXXX XX XXXXXX 4 XX XX XX XX XX XX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The maze is a two-dimensional array of cells. In this display, each cell
is two-characters high and 4 characters wide. There are two blanks in
the top left corner of each cell, and two X’s in the bottom right corner
of each cell. Each cell has a right wall and a bottom wall which may or
may not be open. For example, cell (0,0) has its right wall closed and
its bottom wall open, cell (0,1) has both walls open, cell (1,1) has both
walls closed.
In addition to the cells of the maze, a top wall of X’s and a left wall of XX’s
(with one opening for the maze entrance) comprise the display of the maze.
The bottom wall of the maze is formed by the last row of cells, none of which
have their bottom wall open. The right wall of the maze is formed by the last
column of cells, one of which has its right wall open for the maze exit.
Note that there is only one path between any two cells of the maze. Any cell
can be reached from another cell by a unique path. Such a set of paths between
cells is called a spanning tree. The creation of a maze starts with the all
the cell walls closed and systematically opens cell walls until the complete
spanning tree is formed. The maze entrance and exit are then chosen at
random or fixed locations on opposite sides of the maze.
During the formation of the maze, there will be three types of cells in the
array:
- those that are already in the spanning tree.- those that are not in the spanning tree but are immediately adjacent to some cell in the spanning tree. These cells are called Frontier Cells.- all the other cells, which are neither in the spanning tree nor the frontier.
A bit-field structure, together with some symbolic constants for the
allowed values of the fields, is appropriate to represent a maze cell,
for example:
#define NEITHER 0 /* kinds of cells */#define SPANTREE 1#define FRONTIER 2#define CLOSED 0 /* walls */#define OPEN 1typedef struct { unsigned int kind : 2; /* NEITHER, SPANTREE, or FRONTIER */ unsigned int down : 1; /* CLOSED or OPEN */ unsigned int right: 1; /* CLOSED or OPEN */} Cell;
If cell is a two-dimensional array of these Cell’s, then typical statements
using cell would be:
1. Randomly choose any cell of the array and mark it as a spanning tree cell. The four cells immediately adjacent to it (fewer if it is on an edge or in a corner) are then marked as frontier cells.Repeat Steps 2 and 3 until there are no frontier cells left:2. Randomly choose a frontier cell and connect it to one cell of the current spanning tree by opening one wall. If it is adjacent to more than one cell of the spanning tree (it could be adjacent to as many as four), randomly choose one of them to connect it to and mark the appropriate wall as open. Note: to connect cell[i][j] to the cell above it means cell[i-1][j].down = OPEN;3. Check the cells adjacent to the cell just added to the spanning tree. Any such cells that are neither part of the spanning tree nor the frontier must now be marked as frontier cells.
To be able to easily and efficiently choose a random frontier cell in Step 2,
the row and column indices of the frontier cells must be kept in a circularly
linked list. Such a data structure would be declared like:
typedef struct element { int row, col; struct element *next;} Node;
Example illustrating maze creation
In this example, ++ is used instead of XX for the walls so that the F and S
characters, indicating Frontier and Spanning tree cells, stand out clearly.
Principe Fondamental
L'algorithme de backtracking récursif est une méthode de génération de labyrinthes basée sur la recherche en profondeur (Depth-First Search - DFS). Il crée un labyrinthe en "creusant" des passages à travers une grille initialement remplie de murs.
Fonctionnement Étape par Étape
Initialisation
On commence avec une grille où toutes les cellules ont leurs quatre murs (Nord, Sud, Est, Ouest)
Toutes les cellules sont marquées comme “non visitées”
On choisit une cellule de départ (généralement (0,0))
Exploration
Marquer la cellule courante comme “visitée”
Obtenir la liste des cellules voisines non visitées
Décision
Si des voisins non visités existent:
Choisir un voisin aléatoire
Empiler la cellule courante (pour pouvoir y revenir)
Supprimer le mur entre la cellule courante et le voisin choisi
Faire du voisin la nouvelle cellule courante
Répéter depuis l’étape 2
Si aucun voisin non visité n’existe:
Dépiler une cellule (backtracking/retour en arrière)
Faire de cette cellule la cellule courante
Répéter depuis l’étape 2
Terminaison
L’algorithme se termine quand la pile est vide et qu’il n’y a plus de cellule à explorer
Visualisation de l’Algorithme
Étape 1: Départ en (0,0)┌─┬─┬─┐│S│ │ │ S = Start (cellule de départ)├─┼─┼─┤│ │ │ │├─┼─┼─┤│ │ │ │└─┴─┴─┘Étape 2: Exploration vers l'Est┌─┬─┬─┐│S X│ │ X = cellule visitée├─┼─┼─┤│ │ │ │├─┼─┼─┤│ │ │ │└─┴─┴─┘Étape 3: Backtracking quand bloqué┌─┬─┬─┐│S X X│ On revient en arrière├─┼ ┼─┤ puis on explore Sud│ │X│ │├─┼─┼─┤│ │ │ │└─┴─┴─┘... et ainsi de suite jusqu'à ce quetoutes les cellules soient visitées.
Propriétés Mathématiques
Complexité temporelle: O(n) où n est le nombre de cellules
Complexité spatiale: O(n) pour la pile de backtracking
Garanties:
Chaque cellule est visitée exactement une fois
Le labyrinthe est connexe (tous les points sont accessibles)
Le labyrinthe est “parfait” (sans boucles, un seul chemin entre deux points)
La génération est uniforme (tous les labyrinthes possibles ont la même probabilité)
Avantages et Inconvénients
Avantages:
Simple à mettre en oeuvre
Génère des labyrinthes avec de longs couloirs sinueux
Utilisation efficace de la mémoire
Visuellement intéressant (structure organique)
Inconvénients:
Peut créer des labyrinthes avec un biais directionnel
Les chemins ont tendance à être longs et tortueux
Moins de contrôle sur la difficulté du labyrinthe
Histoire de l'Algorithme
Origines et Développement
L’algorithme de backtracking pour la génération de labyrinthes trouve ses racines dans les années 1960-1970, période dorée de l’exploration des algorithmes de graphes.
Contexte Historique
Années 1960 - Les Prémices
L’algorithme de recherche en profondeur (DFS), base du backtracking, est formalisé par les pionniers de l’informatique
Les premiers ordinateurs permettent d’explorer des algorithmes de génération procédurale
Les labyrinthes deviennent un terrain de jeu pour tester les algorithmes de graphes
Années 1970 - Popularisation
Les ordinateurs personnels émergent, rendant la programmation accessible
Les magazines d’informatique publient des algorithmes de génération de labyrinthes
Les jeux vidéo utilisent des labyrinthes procéduraux (début des roguelikes)
Années 1980-1990 - Raffinement
Étude systématique des différentes méthodes de génération
Comparaison des propriétés visuelles et mathématiques
Le recursive backtracking devient l’algorithme de référence pour les labyrinthes “parfaits”
Pourquoi le Backtracking Récursif?
L’algorithme s’est imposé pour plusieurs raisons:
Élégance conceptuelle - L’idée de “creuser” des tunnels en explorant puis en revenant en arrière est intuitive
Simplicité d’implémentation - Quelques dizaines de lignes suffisent
Qualité esthétique - Produit des labyrinthes avec de longs passages sinueux, visuellement satisfaisants
Performance - Complexité linéaire, très rapide même sur de grandes grilles
Variantes et Évolution
Au fil du temps, d’autres algorithmes ont été développés:
Algorithme de Prim - Croissance aléatoire
Algorithme de Kruskal - Union-find sur les arêtes
Division récursive - Diviser et conquérir
Algorithme d’Eller - Génération ligne par ligne
Mais le backtracking récursif reste le plus populaire pour sa simplicité et ses résultats esthétiques.
Architecture du Code
Structure Globale
MazeGenerator (Classe principale)│├── __init__() # Initialisation de la grille├── generate_maze() # Algorithme de backtracking├── create_entrance_exit() # Création des accès└── create_maze_geometry() # Génération 3D dans Blender │ ├── _create_wall() # Création d'un mur individuel ├── _create_floor() # Création du sol ├── _create_markers() # Marqueurs entrée/sortie └── _get_marker_position() # Calcul de position
Diagramme de Flux
┌─────────────────────────┐│ Initialisation ││ - Créer grille ││ - Tous murs présents │└───────────┬─────────────┘ │ ▼┌─────────────────────────┐│ Génération Labyrinthe ││ - Backtracking DFS ││ - Suppression de murs │└───────────┬─────────────┘ │ ▼┌─────────────────────────┐│ Création Entrée/Sortie ││ - Sélection aléatoire ││ - Sur périmètre │└───────────┬─────────────┘ │ ▼┌─────────────────────────┐│ Génération Géométrie ││ - Création murs 3D ││ - Sol et marqueurs │└─────────────────────────┘
Explication Détaillée du Code
1. Initialisation (__init__)
def __init__(self, width=10, height=10, wall_thickness=0.1, wall_height=2.0, cell_size=2.0): self.width = width self.height = height self.wall_thickness = wall_thickness self.wall_height = wall_height self.cell_size = cell_size # Grille: chaque cellule a 4 murs possibles self.grid = [[{'N': True, 'S': True, 'E': True, 'W': True, 'visited': False} for _ in range(width)] for _ in range(height)] self.entrance = None self.exit = None
Explication:
- La grille est une matrice 2D de dictionnaires
- Chaque cellule stocke l’état de ses 4 murs (True = présent)
- ‘N’, ‘S’, ‘E’, ‘W’ correspondent aux directions cardinales
- visited permet de suivre la progression de l’algorithme
Structure de données:
grid[y][x] = { 'N': True, # Mur Nord 'S': True, # Mur Sud 'E': True, # Mur Est 'W': True, # Mur Ouest 'visited': False # État de visite}
2. Génération du Labyrinthe (generate_maze)
def generate_maze(self): stack = [] current = (0, 0) self.grid[current[1]][current[0]]['visited'] = True while True: neighbors = self._get_unvisited_neighbors(current) if neighbors: # Avancer: choisir un voisin next_cell = random.choice(neighbors) stack.append(current) self._remove_wall(current, next_cell) self.grid[next_cell[1]][next_cell[0]]['visited'] = True current = next_cell elif stack: # Reculer: backtracking current = stack.pop() else: # Terminé: tous les chemins explorés break
Analyse détaillée:
Pile (Stack):
Stocke le chemin parcouru
Permet de revenir en arrière (backtracking)
Structure LIFO (Last In, First Out)
Boucle principale:
Continue tant qu’il reste des cellules dans la pile
Gère deux cas: exploration et backtracking
Choix aléatoire:
random.choice(neighbors) assure l’uniformité
Chaque labyrinthe généré est unique
Invariants:
Une cellule visitée l’est définitivement
La pile contient toujours un chemin valide vers l’origine
À la fin, toutes les cellules sont visitées
3. Recherche de Voisins (_get_unvisited_neighbors)
def _get_unvisited_neighbors(self, cell): x, y = cell neighbors = [] # Nord (y-1) if y > 0 and not self.grid[y-1][x]['visited']: neighbors.append((x, y-1)) # Sud (y+1) if y < self.height - 1 and not self.grid[y+1][x]['visited']: neighbors.append((x, y+1)) # Est (x+1) if x < self.width - 1 and not self.grid[y][x+1]['visited']: neighbors.append((x+1, y)) # Ouest (x-1) if x > 0 and not self.grid[y][x-1]['visited']: neighbors.append((x-1, y)) return neighbors
Points importants:
Vérification des limites de la grille (0 ≤ x < width, 0 ≤ y < height)
Seules les cellules non visitées sont retournées
L’ordre de vérification n’affecte pas l’uniformité (choix aléatoire après)
Système de coordonnées:
x →y ┌─────┬─────┬─────┐↓ │(0,0)│(1,0)│(2,0)│ ├─────┼─────┼─────┤ │(0,1)│(1,1)│(2,1)│ ├─────┼─────┼─────┤ │(0,2)│(1,2)│(2,2)│ └─────┴─────┴─────┘
4. Suppression de Murs (_remove_wall)
def _remove_wall(self, current, next_cell): x1, y1 = current x2, y2 = next_cell if x2 == x1 + 1: # Mouvement Est self.grid[y1][x1]['E'] = False self.grid[y2][x2]['W'] = False elif x2 == x1 - 1: # Mouvement Ouest self.grid[y1][x1]['W'] = False self.grid[y2][x2]['E'] = False elif y2 == y1 + 1: # Mouvement Sud self.grid[y1][x1]['S'] = False self.grid[y2][x2]['N'] = False elif y2 == y1 - 1: # Mouvement Nord self.grid[y1][x1]['N'] = False self.grid[y2][x2]['S'] = False
Principe de symétrie:
Supprimer un mur affecte deux cellules
Le mur Est de A est le mur Ouest de B
Maintient la cohérence de la structure
Visualisation:
Avant: Après:┌───┬───┐ ┌───┬───┐│ A │ B │ → │ A B │└───┴───┘ └───┴───┘ ↑ ↑ Mur entre A et B Mur supprimé
5. Création Entrée/Sortie (create_entrance_exit)
def create_entrance_exit(self): perimeter_walls = [] # Mur Nord (haut) for x in range(self.width): perimeter_walls.append((x, 0, 'N')) # Mur Sud (bas) for x in range(self.width): perimeter_walls.append((x, self.height - 1, 'S')) # Mur Ouest (gauche) for y in range(self.height): perimeter_walls.append((0, y, 'W')) # Mur Est (droite) for y in range(self.height): perimeter_walls.append((self.width - 1, y, 'E')) # Sélection aléatoire random.shuffle(perimeter_walls) entrance_wall = perimeter_walls[0] exit_wall = perimeter_walls[1] # Suppression des murs x, y, direction = entrance_wall self.grid[y][x][direction] = False self.entrance = entrance_wall x, y, direction = exit_wall self.grid[y][x][direction] = False self.exit = exit_wall
Stratégie:
Collecter tous les murs du périmètre
Mélanger aléatoirement la liste
Prendre les deux premiers (garantit qu’ils sont différents)
Nombre de positions possibles:
Mur Nord: width positions
Mur Sud: width positions
Mur Est: height positions
Mur Ouest: height positions
Total:2 × (width + height) positions
6. Génération de la Géométrie 3D
a) Création des Murs (create_maze_geometry)
for y in range(self.height): for x in range(self.width): cell = self.grid[y][x] pos_x = x * self.cell_size pos_y = y * self.cell_size # Mur Nord if cell['N']: self._create_wall(pos_x, pos_y, self.cell_size, self.wall_thickness, maze_collection, mat) # Mur Ouest if cell['W']: self._create_wall(pos_x, pos_y, self.wall_thickness, self.cell_size, maze_collection, mat)
Optimisation:
Seuls les murs intérieurs sont créés lors du parcours
Les murs Sud et Est sont créés uniquement pour la dernière ligne/colonne
Évite la duplication de géométrie
Calcul de position:
Cellule (x, y) → Position 3D (pos_x, pos_y)pos_x = x × cell_sizepos_y = y × cell_sizeExemple: cellule (2, 1) avec cell_size=2.0→ Position (4.0, 2.0) dans l'espace Blender
b) Création d’un Mur (_create_wall)
def _create_wall(self, x, y, size_x, size_y, collection, material): # Créer un cube unitaire bpy.ops.mesh.primitive_cube_add( size=1, location=(x + size_x/2, y + size_y/2, self.wall_height/2) ) wall = bpy.context.active_object # Mise à l'échelle pour la taille désirée wall.scale = (size_x, size_y, self.wall_height) # Application du matériau wall.data.materials.append(material) # Déplacement vers la collection du labyrinthe for coll in wall.users_collection: coll.objects.unlink(wall) collection.objects.link(wall)
Processus Blender:
Création d’un cube avec primitive_cube_add
Positionnement au centre du mur
Mise à l’échelle pour obtenir les bonnes dimensions
Application du matériau bleu
Organisation dans la collection “Maze”
Position du centre:
Pour un mur horizontal: (x + size_x/2, y + wall_thickness/2, height/2)
Pour un mur vertical: (x + wall_thickness/2, y + size_y/2, height/2)
Emission Strength: Intensité lumineuse (0 = pas d’émission)
Effet visuel:
Les marqueurs brillent légèrement dans le rendu
Facilement repérables même dans des labyrinthes complexes
Vert = début (go!), Rouge = fin (stop!)
d) Calcul de Position des Marqueurs
def _get_marker_position(self, wall_info): x, y, direction = wall_info pos_x = x * self.cell_size + self.cell_size / 2 pos_y = y * self.cell_size + self.cell_size / 2 # Ajustement selon la direction if direction == 'N': pos_y = y * self.cell_size elif direction == 'S': pos_y = (y + 1) * self.cell_size elif direction == 'W': pos_x = x * self.cell_size elif direction == 'E': pos_x = (x + 1) * self.cell_size return (pos_x, pos_y)
Logique de positionnement:
Par défaut: centre de la cellule
Ajustement: déplacer vers le mur concerné
Résultat: marqueur exactement sur l’ouverture
Exemple:
Cellule (2, 1), direction 'E', cell_size = 2.0→ Centre: (5.0, 3.0)→ Ajustement Est: pos_x = 3 × 2.0 = 6.0→ Position finale: (6.0, 3.0)
Utilisation et Personnalisation
Paramètres de Base
maze = MazeGenerator( width=15, # Largeur (nombre de cellules en X) height=15, # Hauteur (nombre de cellules en Y) wall_thickness=0.2, # Épaisseur des murs (en unités Blender) wall_height=3.0, # Hauteur des murs cell_size=2.0 # Taille d'une cellule)
# Entrée en bleu au lieu de vertbsdf.inputs["Base Color"].default_value = (0.0, 0.0, 1.0, 1.0)bsdf.inputs["Emission Color"].default_value = (0.0, 0.0, 1.0, 1.0)
2. Ajouter un Plafond
def _create_ceiling(self, collection): """Créer un plafond pour le labyrinthe.""" floor_size_x = self.width * self.cell_size floor_size_y = self.height * self.cell_size bpy.ops.mesh.primitive_plane_add( size=1, location=(floor_size_x/2, floor_size_y/2, self.wall_height) ) ceiling = bpy.context.active_object ceiling.scale = (floor_size_x, floor_size_y, 1) ceiling.name = "MazeCeiling" # ... (appliquer matériau et lier à la collection)
Puis dans create_maze_geometry():
self._create_floor(maze_collection)self._create_ceiling(maze_collection) # Ajouter cette ligne
3. Formes de Marqueurs Différentes
Remplacer les cylindres par d’autres formes:
# Sphère pour l'entréebpy.ops.mesh.primitive_uv_sphere_add( radius=0.5, location=(entrance_pos[0], entrance_pos[1], 0.5))# Cône pour la sortiebpy.ops.mesh.primitive_cone_add( radius1=0.5, depth=1.0, location=(exit_pos[0], exit_pos[1], 0.5))
def generate_maze_prim(self): # Commencer avec une cellule # Ajouter ses murs à une liste # Répéter: # - Choisir un mur aléatoire # - Si il connecte visité/non-visité: le supprimer
Division Récursive
def generate_maze_recursive_division(self): # Diviser l'espace avec un mur # Créer une ouverture dans le mur # Répéter récursivement sur les deux sous-espaces
Fonctionnalités Additionnelles
Génération de Solution
def find_solution(self): """Trouver le chemin de l'entrée à la sortie (BFS).""" # Implémentation du breadth-first search # Retourne une liste de cellules formant le chemin
Niveaux de Difficulté
def set_difficulty(self, level): """Modifier la difficulté en ajoutant/supprimant des murs.""" if level == "easy": # Supprimer 10% des murs restants elif level == "hard": # Garder tous les murs (labyrinthe parfait)
Export vers d’Autres Formats
def export_to_json(self): """Exporter le labyrinthe en JSON.""" return json.dumps({ 'width': self.width, 'height': self.height, 'grid': self.grid, 'entrance': self.entrance, 'exit': self.exit })
Conseils d’Optimisation
Performance pour Grands Labyrinthes
Pour des labyrinthes > 100×100 cellules:
Utiliser des instances pour les murs:
# Créer un mur "template"template_wall = bpy.data.objects["WallTemplate"]# Créer des instances au lieu de nouveaux objetsinstance = template_wall.copy()instance.data = template_wall.data # Partage la géométrieinstance.location = (x, y, z)
Fusion des murs adjacents:
# Au lieu de créer un mur par segment,# fusionner les murs alignés en un seul mesh
LOD (Level of Detail):
# Pour la visualisation à distance,# utiliser des murs low-poly
Intégration dans un Projet
Utilisation comme Module
# maze_generator.py (votre fichier)from maze_generator import MazeGenerator# Dans votre script principalmaze = MazeGenerator(20, 20)maze.generate_maze()maze.create_entrance_exit()maze.create_maze_geometry("MyMaze")# Accéder aux donnéesentrance_cell = maze.entranceprint(f"Entrée en position: {entrance_cell}")
Génération Procédurale dans un Jeu
# Générer un nouveau niveau à chaque partieimport timeseed = int(time.time())random.seed(seed)maze = MazeGenerator(25, 25)maze.generate_maze()# ... utiliser le labyrinthe dans votre jeu
Ressources Supplémentaires
Lectures Recommandées
Algorithmes de Labyrinthes:
“Mazes for Programmers” par Jamis Buck
“Think Labyrinth!” par Walter D. Pullen
Backtracking et Graphes:
“Introduction to Algorithms” par Cormen et al. (CLRS)
“The Algorithm Design Manual” par Steven Skiena
Code Golf et Optimisation:
Les travaux de John Tromp sur la complexité de Kolmogorov
Code Golf Stack Exchange (communauté)
Communautés
r/proceduralgeneration - Reddit sur la génération procédurale
Blender Artists Forum - Discussion sur Python et Blender
Stack Overflow - Questions techniques
Outils Connexes
MazeSolver - Visualiseur d’algorithmes de recherche de chemins
Procedural Toolkit for Unity - Générateurs de labyrinthes pour Unity
Processing - Framework pour art génératif
Le code pour Blender
Copiez/coller et exécutez dans un script Blender:
"""Blender Maze GeneratorA Python script to generate 3D mazes in Blender using recursive backtracking algorithm."""import bpyimport randomclass MazeGenerator: def __init__(self, width=10, height=10, wall_thickness=0.1, wall_height=2.0, cell_size=2.0): """ Initialize the maze generator. Args: width: Number of cells in X direction height: Number of cells in Y direction wall_thickness: Thickness of walls wall_height: Height of walls cell_size: Size of each cell """ self.width = width self.height = height self.wall_thickness = wall_thickness self.wall_height = wall_height self.cell_size = cell_size # Initialize maze grid (True = wall, False = path) self.grid = [[{'N': True, 'S': True, 'E': True, 'W': True, 'visited': False} for _ in range(width)] for _ in range(height)] # Store entrance and exit positions self.entrance = None self.exit = None def generate_maze(self): """Generate maze using recursive backtracking algorithm.""" stack = [] current = (0, 0) self.grid[current[1]][current[0]]['visited'] = True while True: neighbors = self._get_unvisited_neighbors(current) if neighbors: # Choose random neighbor next_cell = random.choice(neighbors) stack.append(current) # Remove wall between current and next cell self._remove_wall(current, next_cell) # Mark next cell as visited and move to it self.grid[next_cell[1]][next_cell[0]]['visited'] = True current = next_cell elif stack: # Backtrack current = stack.pop() else: # Maze complete break def _get_unvisited_neighbors(self, cell): """Get list of unvisited neighboring cells.""" x, y = cell neighbors = [] # Check North if y > 0 and not self.grid[y-1][x]['visited']: neighbors.append((x, y-1)) # Check South if y < self.height - 1 and not self.grid[y+1][x]['visited']: neighbors.append((x, y+1)) # Check East if x < self.width - 1 and not self.grid[y][x+1]['visited']: neighbors.append((x+1, y)) # Check West if x > 0 and not self.grid[y][x-1]['visited']: neighbors.append((x-1, y)) return neighbors def _remove_wall(self, current, next_cell): """Remove wall between two cells.""" x1, y1 = current x2, y2 = next_cell if x2 == x1 + 1: # Moving East self.grid[y1][x1]['E'] = False self.grid[y2][x2]['W'] = False elif x2 == x1 - 1: # Moving West self.grid[y1][x1]['W'] = False self.grid[y2][x2]['E'] = False elif y2 == y1 + 1: # Moving South self.grid[y1][x1]['S'] = False self.grid[y2][x2]['N'] = False elif y2 == y1 - 1: # Moving North self.grid[y1][x1]['N'] = False self.grid[y2][x2]['S'] = False def create_entrance_exit(self): """Create random entrance and exit on perimeter walls.""" perimeter_walls = [] # Top wall (North side) for x in range(self.width): perimeter_walls.append((x, 0, 'N')) # Bottom wall (South side) for x in range(self.width): perimeter_walls.append((x, self.height - 1, 'S')) # Left wall (West side) for y in range(self.height): perimeter_walls.append((0, y, 'W')) # Right wall (East side) for y in range(self.height): perimeter_walls.append((self.width - 1, y, 'E')) # Randomly select entrance and exit random.shuffle(perimeter_walls) entrance_wall = perimeter_walls[0] exit_wall = perimeter_walls[1] # Remove walls for entrance and exit x, y, direction = entrance_wall self.grid[y][x][direction] = False self.entrance = entrance_wall x, y, direction = exit_wall self.grid[y][x][direction] = False self.exit = exit_wall print(f"Entrance: Cell ({self.entrance[0]}, {self.entrance[1]}) - {self.entrance[2]} wall") print(f"Exit: Cell ({self.exit[0]}, {self.exit[1]}) - {self.exit[2]} wall") def create_maze_geometry(self, collection_name="Maze"): """Create 3D geometry for the maze in Blender.""" # Create new collection for maze if collection_name in bpy.data.collections: maze_collection = bpy.data.collections[collection_name] else: maze_collection = bpy.data.collections.new(collection_name) bpy.context.scene.collection.children.link(maze_collection) # Create material for walls mat = bpy.data.materials.new(name="MazeMaterial") mat.use_nodes = True mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.2, 0.3, 0.5, 1.0) # Create walls for y in range(self.height): for x in range(self.width): cell = self.grid[y][x] pos_x = x * self.cell_size pos_y = y * self.cell_size # North wall if cell['N']: self._create_wall( pos_x, pos_y, self.cell_size, self.wall_thickness, maze_collection, mat ) # West wall if cell['W']: self._create_wall( pos_x, pos_y, self.wall_thickness, self.cell_size, maze_collection, mat ) # South wall (only for last row) if y == self.height - 1 and cell['S']: self._create_wall( pos_x, pos_y + self.cell_size, self.cell_size, self.wall_thickness, maze_collection, mat ) # East wall (only for last column) if x == self.width - 1 and cell['E']: self._create_wall( pos_x + self.cell_size, pos_y, self.wall_thickness, self.cell_size, maze_collection, mat ) # Create floor self._create_floor(maze_collection) # Add entrance and exit markers self._create_markers(maze_collection) print(f"Maze generated: {self.width}x{self.height} cells") def _create_wall(self, x, y, size_x, size_y, collection, material): """Create a single wall segment.""" bpy.ops.mesh.primitive_cube_add( size=1, location=(x + size_x/2, y + size_y/2, self.wall_height/2) ) wall = bpy.context.active_object wall.scale = (size_x, size_y, self.wall_height) # Apply material if wall.data.materials: wall.data.materials[0] = material else: wall.data.materials.append(material) # Link to collection for coll in wall.users_collection: coll.objects.unlink(wall) collection.objects.link(wall) def _create_floor(self, collection): """Create floor for the maze.""" floor_size_x = self.width * self.cell_size floor_size_y = self.height * self.cell_size bpy.ops.mesh.primitive_plane_add( size=1, location=(floor_size_x/2, floor_size_y/2, 0) ) floor = bpy.context.active_object floor.scale = (floor_size_x, floor_size_y, 1) floor.name = "MazeFloor" # Create floor material mat = bpy.data.materials.new(name="FloorMaterial") mat.use_nodes = True mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.8, 0.8, 0.8, 1.0) if floor.data.materials: floor.data.materials[0] = mat else: floor.data.materials.append(mat) # Link to collection for coll in floor.users_collection: coll.objects.unlink(floor) collection.objects.link(floor) def _create_markers(self, collection): """Create visual markers for entrance and exit.""" if not self.entrance or not self.exit: print("Warning: Entrance or exit not set!") return print(f"Creating entrance marker at {self.entrance}") print(f"Creating exit marker at {self.exit}") # Create entrance marker (green) entrance_pos = self._get_marker_position(self.entrance) bpy.ops.mesh.primitive_cylinder_add( radius=0.5, depth=0.3, location=(entrance_pos[0], entrance_pos[1], 0.15) ) entrance_marker = bpy.context.active_object entrance_marker.name = "Entrance_Marker" # Green material for entrance mat_entrance = bpy.data.materials.new(name="EntranceMaterial") mat_entrance.use_nodes = True bsdf = mat_entrance.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = (0.0, 1.0, 0.0, 1.0) bsdf.inputs["Emission Color"].default_value = (0.0, 1.0, 0.0, 1.0) bsdf.inputs["Emission Strength"].default_value = 2.0 entrance_marker.data.materials.append(mat_entrance) # Move to collection for coll in entrance_marker.users_collection: coll.objects.unlink(entrance_marker) collection.objects.link(entrance_marker) # Create exit marker (red) exit_pos = self._get_marker_position(self.exit) bpy.ops.mesh.primitive_cylinder_add( radius=0.5, depth=0.3, location=(exit_pos[0], exit_pos[1], 0.15) ) exit_marker = bpy.context.active_object exit_marker.name = "Exit_Marker" # Red material for exit mat_exit = bpy.data.materials.new(name="ExitMaterial") mat_exit.use_nodes = True bsdf = mat_exit.node_tree.nodes["Principled BSDF"] bsdf.inputs["Base Color"].default_value = (1.0, 0.0, 0.0, 1.0) bsdf.inputs["Emission Color"].default_value = (1.0, 0.0, 0.0, 1.0) bsdf.inputs["Emission Strength"].default_value = 2.0 exit_marker.data.materials.append(mat_exit) # Move to collection for coll in exit_marker.users_collection: coll.objects.unlink(exit_marker) collection.objects.link(exit_marker) print("Entrance and exit markers created successfully!") def _get_marker_position(self, wall_info): """Calculate position for entrance/exit marker.""" x, y, direction = wall_info pos_x = x * self.cell_size + self.cell_size / 2 pos_y = y * self.cell_size + self.cell_size / 2 # Adjust position based on wall direction if direction == 'N': pos_y = y * self.cell_size elif direction == 'S': pos_y = (y + 1) * self.cell_size elif direction == 'W': pos_x = x * self.cell_size elif direction == 'E': pos_x = (x + 1) * self.cell_size return (pos_x, pos_y)def clear_maze_collection(collection_name="Maze"): """Clear existing maze collection.""" if collection_name in bpy.data.collections: collection = bpy.data.collections[collection_name] for obj in collection.objects: bpy.data.objects.remove(obj, do_unlink=True) bpy.data.collections.remove(collection)# Main executionif __name__ == "__main__": # Clear any existing maze clear_maze_collection("Maze") # Create and generate maze # Adjust parameters here: maze = MazeGenerator( width=15, # Number of cells in X height=15, # Number of cells in Y wall_thickness=0.2, # Wall thickness wall_height=3.0, # Wall height cell_size=2.0 # Size of each cell ) maze.generate_maze() maze.create_entrance_exit() maze.create_maze_geometry("Maze") print("Maze generation complete!")
Code python pour Unreal Engine
Créez un fichier intitulé unreal_maze_generator.py
"""Unreal Engine Maze GeneratorA Python script to generate 3D mazes in Unreal Engine using recursive backtracking algorithm.Requires: Unreal Engine with Python Editor Script Plugin enabled"""import unrealimport randomclass UnrealMazeGenerator: def __init__(self, width=10, height=10, wall_thickness=20.0, wall_height=300.0, cell_size=200.0): """ Initialize the maze generator for Unreal Engine. Args: width: Number of cells in X direction height: Number of cells in Y direction wall_thickness: Thickness of walls in cm (Unreal units) wall_height: Height of walls in cm cell_size: Size of each cell in cm """ self.width = width self.height = height self.wall_thickness = wall_thickness self.wall_height = wall_height self.cell_size = cell_size # Initialize maze grid self.grid = [[{'N': True, 'S': True, 'E': True, 'W': True, 'visited': False} for _ in range(width)] for _ in range(height)] # Store entrance and exit positions self.entrance = None self.exit = None # Unreal Editor utilities self.editor_level_lib = unreal.EditorLevelLibrary() self.editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) # Store spawned actors for organization self.maze_actors = [] def generate_maze(self): """Generate maze using recursive backtracking algorithm.""" stack = [] current = (0, 0) self.grid[current[1]][current[0]]['visited'] = True while True: neighbors = self._get_unvisited_neighbors(current) if neighbors: # Choose random neighbor next_cell = random.choice(neighbors) stack.append(current) # Remove wall between current and next cell self._remove_wall(current, next_cell) # Mark next cell as visited and move to it self.grid[next_cell[1]][next_cell[0]]['visited'] = True current = next_cell elif stack: # Backtrack current = stack.pop() else: # Maze complete break def _get_unvisited_neighbors(self, cell): """Get list of unvisited neighboring cells.""" x, y = cell neighbors = [] # Check North if y > 0 and not self.grid[y-1][x]['visited']: neighbors.append((x, y-1)) # Check South if y < self.height - 1 and not self.grid[y+1][x]['visited']: neighbors.append((x, y+1)) # Check East if x < self.width - 1 and not self.grid[y][x+1]['visited']: neighbors.append((x+1, y)) # Check West if x > 0 and not self.grid[y][x-1]['visited']: neighbors.append((x-1, y)) return neighbors def _remove_wall(self, current, next_cell): """Remove wall between two cells.""" x1, y1 = current x2, y2 = next_cell if x2 == x1 + 1: # Moving East self.grid[y1][x1]['E'] = False self.grid[y2][x2]['W'] = False elif x2 == x1 - 1: # Moving West self.grid[y1][x1]['W'] = False self.grid[y2][x2]['E'] = False elif y2 == y1 + 1: # Moving South self.grid[y1][x1]['S'] = False self.grid[y2][x2]['N'] = False elif y2 == y1 - 1: # Moving North self.grid[y1][x1]['N'] = False self.grid[y2][x2]['S'] = False def create_entrance_exit(self): """Create random entrance and exit on perimeter walls.""" perimeter_walls = [] # Top wall (North side) for x in range(self.width): perimeter_walls.append((x, 0, 'N')) # Bottom wall (South side) for x in range(self.width): perimeter_walls.append((x, self.height - 1, 'S')) # Left wall (West side) for y in range(self.height): perimeter_walls.append((0, y, 'W')) # Right wall (East side) for y in range(self.height): perimeter_walls.append((self.width - 1, y, 'E')) # Randomly select entrance and exit random.shuffle(perimeter_walls) entrance_wall = perimeter_walls[0] exit_wall = perimeter_walls[1] # Remove walls for entrance and exit x, y, direction = entrance_wall self.grid[y][x][direction] = False self.entrance = entrance_wall x, y, direction = exit_wall self.grid[y][x][direction] = False self.exit = exit_wall unreal.log(f"Entrance: Cell ({self.entrance[0]}, {self.entrance[1]}) - {self.entrance[2]} wall") unreal.log(f"Exit: Cell ({self.exit[0]}, {self.exit[1]}) - {self.exit[2]} wall") def create_maze_in_level(self, folder_name="/Game/Maze"): """Create the maze geometry in the Unreal level.""" unreal.log("Creating maze geometry in Unreal Engine...") # Create materials wall_material = self._create_wall_material() floor_material = self._create_floor_material() # Create walls wall_count = 0 for y in range(self.height): for x in range(self.width): cell = self.grid[y][x] pos_x = x * self.cell_size pos_y = y * self.cell_size # North wall if cell['N']: self._create_wall( pos_x, pos_y, self.cell_size, self.wall_thickness, wall_material, f"Wall_N_{x}_{y}" ) wall_count += 1 # West wall if cell['W']: self._create_wall( pos_x, pos_y, self.wall_thickness, self.cell_size, wall_material, f"Wall_W_{x}_{y}" ) wall_count += 1 # South wall (only for last row) if y == self.height - 1 and cell['S']: self._create_wall( pos_x, pos_y + self.cell_size, self.cell_size, self.wall_thickness, wall_material, f"Wall_S_{x}_{y}" ) wall_count += 1 # East wall (only for last column) if x == self.width - 1 and cell['E']: self._create_wall( pos_x + self.cell_size, pos_y, self.wall_thickness, self.cell_size, wall_material, f"Wall_E_{x}_{y}" ) wall_count += 1 # Create floor self._create_floor(floor_material) # Create entrance and exit markers self._create_markers() unreal.log(f"Maze generation complete! Created {wall_count} walls") unreal.log(f"Maze dimensions: {self.width}x{self.height} cells") unreal.log(f"Total actors spawned: {len(self.maze_actors)}") def _create_wall_material(self): """Create a blue material for walls.""" # In a real scenario, you would load or create a material asset # For simplicity, we'll return None and set color via actor properties return None def _create_floor_material(self): """Create a gray material for floor.""" return None def _create_wall(self, x, y, size_x, size_y, material, name): """ Create a single wall segment using a Cube static mesh. Note: In Unreal, coordinates are X=forward, Y=right, Z=up We're treating our maze as X-Y plane (top-down view) """ # Calculate center position center_x = x + size_x / 2 center_y = y + size_y / 2 center_z = self.wall_height / 2 # Create location (Unreal uses cm by default) location = unreal.Vector(center_x, center_y, center_z) rotation = unreal.Rotator(0, 0, 0) scale = unreal.Vector(size_x / 100.0, size_y / 100.0, self.wall_height / 100.0) # Spawn a cube actor actor_class = unreal.EditorAssetLibrary.load_blueprint_class('/Engine/BasicShapes/Cube.Cube') if actor_class: spawned_actor = self.editor_level_lib.spawn_actor_from_class( actor_class, location, rotation ) if spawned_actor: # Set scale spawned_actor.set_actor_scale3d(scale) # Set name spawned_actor.set_actor_label(name) # Add to our list self.maze_actors.append(spawned_actor) # Set color (blue-ish) # This would require setting up material instances in practice else: # Fallback: create a static mesh actor with cube spawned_actor = self.editor_level_lib.spawn_actor_from_object( unreal.load_object(None, '/Engine/BasicShapes/Cube.Cube'), location, rotation ) if spawned_actor: spawned_actor.set_actor_scale3d(scale) spawned_actor.set_actor_label(name) self.maze_actors.append(spawned_actor) def _create_floor(self, material): """Create floor for the maze.""" floor_size_x = self.width * self.cell_size floor_size_y = self.height * self.cell_size # Center position center_x = floor_size_x / 2 center_y = floor_size_y / 2 center_z = -10.0 # Slightly below ground location = unreal.Vector(center_x, center_y, center_z) rotation = unreal.Rotator(0, 0, 0) scale = unreal.Vector(floor_size_x / 100.0, floor_size_y / 100.0, 0.2) # Spawn floor cube actor_class = unreal.EditorAssetLibrary.load_blueprint_class('/Engine/BasicShapes/Cube.Cube') if actor_class: floor_actor = self.editor_level_lib.spawn_actor_from_class( actor_class, location, rotation ) if floor_actor: floor_actor.set_actor_scale3d(scale) floor_actor.set_actor_label("MazeFloor") self.maze_actors.append(floor_actor) def _create_markers(self): """Create visual markers for entrance and exit.""" if not self.entrance or not self.exit: unreal.log_warning("Entrance or exit not set!") return unreal.log(f"Creating entrance marker at {self.entrance}") unreal.log(f"Creating exit marker at {self.exit}") # Create entrance marker (green cylinder) entrance_pos = self._get_marker_position(self.entrance) self._create_marker(entrance_pos[0], entrance_pos[1], "Entrance_Marker", is_entrance=True) # Create exit marker (red cylinder) exit_pos = self._get_marker_position(self.exit) self._create_marker(exit_pos[0], exit_pos[1], "Exit_Marker", is_entrance=False) unreal.log("Entrance and exit markers created successfully!") def _create_marker(self, x, y, name, is_entrance=True): """Create a cylinder marker at the specified position.""" location = unreal.Vector(x, y, 15.0) # 15cm above floor rotation = unreal.Rotator(0, 0, 0) scale = unreal.Vector(0.5, 0.5, 0.3) # Radius 50cm, height 30cm # Load cylinder mesh actor_class = unreal.EditorAssetLibrary.load_blueprint_class('/Engine/BasicShapes/Cylinder.Cylinder') if actor_class: marker = self.editor_level_lib.spawn_actor_from_class( actor_class, location, rotation ) if marker: marker.set_actor_scale3d(scale) marker.set_actor_label(name) self.maze_actors.append(marker) # In practice, you would apply a green/red emissive material here def _get_marker_position(self, wall_info): """Calculate position for entrance/exit marker.""" x, y, direction = wall_info pos_x = x * self.cell_size + self.cell_size / 2 pos_y = y * self.cell_size + self.cell_size / 2 # Adjust position based on wall direction if direction == 'N': pos_y = y * self.cell_size elif direction == 'S': pos_y = (y + 1) * self.cell_size elif direction == 'W': pos_x = x * self.cell_size elif direction == 'E': pos_x = (x + 1) * self.cell_size return (pos_x, pos_y) def clear_maze(self): """Clear all spawned maze actors from the level.""" unreal.log("Clearing previous maze actors...") for actor in self.maze_actors: if actor: self.editor_level_lib.destroy_actor(actor) self.maze_actors.clear() unreal.log("Maze cleared!") def select_all_maze_actors(self): """Select all maze actors for easy manipulation.""" self.editor_level_lib.set_selected_level_actors(self.maze_actors) unreal.log(f"Selected {len(self.maze_actors)} maze actors")def clear_existing_maze(): """Helper function to clear any existing maze actors.""" # Find all actors with "Wall_", "MazeFloor", or markers in their name all_actors = unreal.EditorLevelLibrary.get_all_level_actors() maze_actors = [actor for actor in all_actors if actor and (actor.get_actor_label().startswith("Wall_") or actor.get_actor_label().startswith("Maze") or actor.get_actor_label().endswith("_Marker"))] for actor in maze_actors: unreal.EditorLevelLibrary.destroy_actor(actor) unreal.log(f"Cleared {len(maze_actors)} existing maze actors")# Main executionif __name__ == "__main__": unreal.log("=" * 60) unreal.log("UNREAL ENGINE MAZE GENERATOR") unreal.log("=" * 60) # Clear any existing maze clear_existing_maze() # Create and generate maze # Adjust parameters here: maze = UnrealMazeGenerator( width=15, # Number of cells in X height=15, # Number of cells in Y wall_thickness=20.0, # Wall thickness in cm wall_height=300.0, # Wall height in cm (3 meters) cell_size=200.0 # Size of each cell in cm (2 meters) ) unreal.log("Generating maze structure...") maze.generate_maze() unreal.log("Creating entrance and exit...") maze.create_entrance_exit() unreal.log("Building maze geometry in level...") maze.create_maze_in_level() unreal.log("=" * 60) unreal.log("MAZE GENERATION COMPLETE!") unreal.log("=" * 60) unreal.log("TIP: Select all maze actors by uncommenting the line below:") unreal.log("# maze.select_all_maze_actors()")
Ne faite pas de dons, si vous voulez payer, achetez la version premium plutôt.
Démarrez Blender.
Installez l’addon dans Edit-Preferences
Dans les paramètres de l’addon Blosm, spécifiez le path qui va être utilisé pour stocker les fichiers comme les terrains, etc.
Aussi, cliquez sur Get it! pour obtenir des clés API des différents services utilisés (ArcGIS, Mapbox et Google 3D Tiles)
Ouvrez le N menu, cherchez Blosm et cliquez sur Import. La ville de Leipzig, d’où vient l’auteur du Add-on va apparaître!
Faites Select, choisissez une petite région au départ (quelques rues seulement!) puis faites Copy des coordonnées
Ensuite, Paste des coordonnées dans Blender, voici le Mont-Royal via OpenStreetMap (gratuit):
Et le campus McGill, le point de vue que j’ai dessiné la dernière fois (avec les Anim 1, on est allé dessiner dans le campus):
Donc avec OpenStreetMap, il manquera beaucoup de détails comme vous pouvez le voir.
Avec les données de Google, surtout en High Level of Details:
Pour utiliser les données de Google Cities 3D, il faut entrer une carte de crédit. Vous avez droit à un certain nombre de downloads avant de devoir payer, si vous l’utilisez quelques fois par mois, y’a aucun souci. Le résultat est bluffant.
Il faut activer la clé de l’API en appuyant sur GetIt et suivre les instructions chiants et compliqués de Google.
Suivez ce tuto si vous voulez activer une clé API Google Maps:
À l’aide de ou de l’engin temps-réel ou logiciel(s) de votre choix (UE, Unity, Godot, Blender, Maya, React, Babylon.js, Three.js, Python, D3.js, etc.), que pourriez vous faire pour montrer la fréquentation géolocalisée de ces données ?