Les connaissances ne devraient pas être brevetées, elles sont faites pour être partagées

Karim Aktouf

Dernière mise à jour: 2025-09-16

# DataViz & Innovation I

# Cours 1

Banque de cours - Historique 2

Historique

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.

Le génial Nael Shiab (Radio-Canada et CBC)

https://ici.radio-canada.ca/info/2022/03/etalement-urbain-densite-population-villes-transport-commun-changements-climatiques/en

https://ici.radio-canada.ca/info/2022/07/ilots-chaleur-villes-inegalites-injustice-changements-climatiques/en

https://manpopex.us/

Lien vers l'original

Banque de cours - Quelques banques de données publiques

Quelques banques de données publiques

🌍 Mondial

OrganizationDomainLink
UN Data / UNStatsDemographics, health, economydata.un.org
World Bank Open DataEconomy, poverty, trade, developmentdata.worldbank.org
OECD DataSocial, labor, innovation, economydata.oecd.org
IMF DataFinance, debt, tradeimf.org/en/Data
FAO (Food & Agriculture)Agriculture, food securityfao.org/statistics
WHO (World Health Organization)Health, disease, life expectancywho.int/data
IPCC Climate DataClimate projectionsipcc-data.org
International Energy Agency (IEA)Energy, renewables, emissionsiea.org/data-and-statistics
ITU (Telecommunications)Internet, ICT adoptionitu.int/en/ITU-D/Statistics

🇫🇷 France

OrganizationDomainLink
INSEEEconomy, population, housinginsee.fr/en/statistiques
Data.gouv.frNational open data portaldata.gouv.fr
Ministère de la Transition ÉcologiqueClimate, energy, transportstatistiques.developpement-durable.gouv.fr
Météo-FranceWeather, climatedonneespubliques.meteofrance.fr
Banque de FranceEconomy, financebanque-france.fr/statistiques

🇨🇦 Canada

OrganizationDomainLink
Statistics CanadaDemographics, economy, societystatcan.gc.ca/data
Government of Canada Open DataFederal datasetsopen.canada.ca
Bank of CanadaFinance, monetary policybankofcanada.ca/rates/indicators
Environment and Climate Change CanadaClimate, environmentclimate.weather.gc.ca
Canadian Institute for Health Information (CIHI)Health systemcihi.ca/en

⚜️ Québec

OrganizationDomainLink
Institut de la statistique du Québec (ISQ)Demographics, economy, laborstat.gouv.qc.ca
Données Québec (open data)Provincial & municipal datasetsdonneesquebec.ca
Hydro-QuébecEnergy production/consumptionhydroquebec.com/data
Ministère de la Santé et des Services sociaux (MSSS)Health, public servicesmsss.gouv.qc.ca
Ministère de l’Environnement et Lutte contre les changements climatiquesEnvironment, climateenvironnement.gouv.qc.ca

🏙️ Montréal

Organization / PortalDomainLink
Ville de Montréal – Données ouvertesOfficial open data portal: urban planning, culture, infrastructure, transport, financesdonnees.montreal.ca
STM (Société de transport de Montréal)Public transport schedules, bus/metro networks, ridershipstm.info/en/about/developers
ExoSuburban trains and buses around Greater Montréalexo.quebec/en/about/open-data
Bixi MontréalBike-sharing data: stations, usage, historical trip datasetsbixi.com/en/open-data
SPVM (Service de police de la Ville de Montréal)Crime, safety, incident statistics (partly via Données Montréal)donnees.montreal.ca/organization/spvm
Santé Montréal (CIUSSS, Santé publique)Public health, environment, population indicatorssantemontreal.qc.ca
Montréal Open Environmental DataAir quality, water quality, green spaces, waste managementdonnees.montreal.ca/en/group/environnement
Port of MontréalTrade, shipping, logistics statisticsport-montreal.com/en/statistics.html
Lien vers l'original

Banque de cours - Blender et Python 1

Blender et Python 1

Blender et Python, Intro

Installation de Python - Ça vient avec Blender

  • Installer Blender 5.1.1 (blender.org)
  • Démarrer un PowerShell (Win-R ou taper “PowerShell” dans le search bar) et tapez la commande suivante pour vérifier la présence de Python:
C:\'Program Files\Blender Foundation\Blender 5.1'\5.1\python\bin\python --version

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 bpy
 
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))
  • 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

https://karimaktouf.ca/graphs/bpy-module-map/

Essayez ces exemples de code

import bpy
 
# Select all objects in the scene
bpy.ops.object.select_all(action='SELECT')
 
# Delete all selected objects
bpy.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 bpy
import random
 
# Clear the current scene by selecting and deleting all objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
 
# Define the number of primitives to create along X and Y axes
grid_size_x = 5
grid_size_y = 5
spacing = 3
 
# Create primitives in a grid
for 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 camera
bpy.ops.object.camera_add(location=(10, -15, 10))
camera = bpy.context.object
camera.rotation_euler = (1.1, 0, 0.8)
 
# Adjust lighting
bpy.ops.object.light_add(type='POINT', radius=1, location=(10, 10, 10))
light = bpy.context.object
light.data.energy = 1000
#
# Sinusoïdale
#
 
import bpy
import math
 
# Clear the current scene by selecting and deleting all objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
 
# Animation settings
frame_start = 1
frame_end = 250
bpy.context.scene.frame_start = frame_start
bpy.context.scene.frame_end = frame_end
 
# Define the grid size and spacing
grid_size_x = 10
grid_size_y = 10
spacing = 2
 
# Create a list to store the spheres
spheres = []
 
# Create spheres in a grid pattern
for 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 effect
for 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 animation
bpy.ops.object.camera_add(location=(12, -20, 15))
camera = bpy.context.object
camera.rotation_euler = (1.1, 0, 0.8)
 
# Set active camera
bpy.context.scene.camera = camera
 
# Add lighting to the scene
bpy.ops.object.light_add(type='SUN', location=(15, -10, 20))
light = bpy.context.object
light.data.energy = 5
#
# Petit robot avec Material
#
 
import bpy
 
# Clear the current scene by selecting and deleting all objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
 
# Function to create a material with a given color
def 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 robot
body_material = create_material("BodyMaterial", (0.1, 0.5, 0.8, 1))  # Blueish
head_material = create_material("HeadMaterial", (0.8, 0.8, 0.8, 1))  # Light Grey
eye_material = create_material("EyeMaterial", (0, 0, 0, 1))  # Black
arm_leg_material = create_material("ArmLegMaterial", (0.3, 0.3, 0.3, 1))  # Dark Grey
antenna_material = create_material("AntennaMaterial", (1, 0, 0, 1))  # Red
 
# Create the robot body
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 1))
body = bpy.context.object
body.scale = (1, 0.6, 1.5)  # Reshape the cube to form the body
body.data.materials.append(body_material)  # Apply material
 
# Create the robot head
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.75, location=(0, 0, 3.25))
head = bpy.context.object
head.data.materials.append(head_material)  # Apply material
 
# Create the robot eyes
for 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 arms
for 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 legs
for 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 antenna
bpy.ops.mesh.primitive_cylinder_add(radius=0.05, depth=1.5, location=(0, 0, 4.5))
antenna = bpy.context.object
antenna.data.materials.append(antenna_material)  # Apply material
 
# Create the antenna top sphere
bpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(0, 0, 5.25))
antenna_top = bpy.context.object
antenna_top.data.materials.append(antenna_material)  # Apply material
 
# Set up lighting
bpy.ops.object.light_add(type='AREA', radius=5, location=(5, -5, 6))
light = bpy.context.object
light.data.energy = 1000  # Increase light intensity
 
# Set up camera
bpy.ops.object.camera_add(location=(7, -7, 5))
camera = bpy.context.object
camera.rotation_euler = (1.1, 0, 0.8)
 
# Set the camera as the active camera
bpy.context.scene.camera = camera
#
# Fibonnaci
#
import bpy
import math
 
# Clear the current scene by selecting and deleting all objects
bpy.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)
Lien vers l'original

# Cours 2

Banque de cours - Blender et Python 2

# Blender et Python 2

Python et les arbres de Montréal

Ces deux pages ouèbe vont nous être très utiles, bookmarquez-les

Téléchargement d’un CSV - les arbres de Montréal

  • Le fichier CSV que l’on va analyser se trouve ici: https://drive.google.com/file/d/1-Z_wJQGVzgibD86AsNV1e6ao1yEw249w/view?usp=sharing
  • 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 os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 bars
for 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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 kes
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 bars
for 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 bpy
import os
from datetime import datetime
import csv
import collections
import pprint
import math
 
 
 
# Montreal Trees CSV file
TREES_CSV = os.path.join(os.path.abspath(os.getcwd()), "arbres-publics.csv")
 
# CSV data set columns
COL = {
    '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 keys
TOTAL = 'Total'
ESSENCE = 'Essence'
SIGLE = 'Sigle'
 
# The maximum number of trees to display
MAX_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 tree
trees_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 order
sorted_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 scene
bpy.ops.object.select_all(action='SELECT')
 
output("Cleaning up the scene")
 
# Delete all selected objects
bpy.ops.object.delete()
 
output("Purging unused data if any")
 
# Clear out any meshes, materials, etc. that might still be in memory
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
 
 
# Bar properties
bar_height = 1.0  # Fixed height (Y axis)
bar_depth = 0.5   # Fixed depth (Z axis)
spacing = 2.2     # Space between bars on the Y axis
scale=10
 
# Find max and min TOTAL to normalize the data
max_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 animation
bpy.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 bars
for 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 camera
cam_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 lense
cam_obj.rotation_euler = (0, 12 * math.pi/180, 0)  # Rotate to face the graph (along -Z axis)
cam_obj.data.lens = 35
cam_obj.location = (scale + MAX_TREES, -MAX_TREES, MAX_TREES * scale/1.5)  # Adjust camera position based on the largest bar
 
 
# Make the camera active
bpy.context.scene.camera = cam_obj
 
 
# Set up lighting
light_data = bpy.data.lights.new(name="Light", type='AREA')
light = bpy.data.objects.new(name="Light", object_data=light_data)
light.data.energy = 500
light.location = (10, -MAX_TREES, 10)
bpy.context.collection.objects.link(light)
 
 
 
 
 
#
# Time for some animation!
#
start_frame = 1
end_frame = 120
min_rotation = -30
max_rotation = 30
 
bpy.context.scene.frame_start = start_frame
bpy.context.scene.frame_end = end_frame
 
 
def 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-axis
 
 
setKeyframe(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 linear
for fcurve in empty.animation_data.action.fcurves:
    for keyframe in fcurve.keyframe_points:
        keyframe.interpolation = 'BEZIER'  # Set interpolation to linear
 
 
bpy.context.scene.frame_set(1)
 
# Play the animation
bpy.ops.screen.animation_play()

Resultat

Lien vers l'original

# Cours 3

Banque de cours - Dataviz Jam I - EULA

# Dataviz Jam I - EULA

  • Allez chercher les EULA (End User License Agreement) suivants: https://drive.google.com/drive/folders/1ypVuspTgfClIrhcg4sTT3-Wj9bXftOaG?usp=sharing
  • Qu’est-ce qu’on peut mesurer?
  • Comment le mesurer procéduralement?
  • Comment visualiser les EULAs?
  • Go!
  • Critères:
    • Tout doit être procédural, y compris les mouvements de la caméra!
    • Lecture des fichiers procédurale et calculs procéduraux (Json, CSV, etc.)
    • Entièrement procédural mais vous choisissez votre arme (BluePrints, Geo Nodes, Python, C++, C#)
    • On peut modifier des paramètres pour regénérer automagiquement (taille, nombre de colonnes, etc.)
    • On peut ajouter automagiquement un nouvel EULA et tout va être recalculé
  • Justement, ajoutez 2 nouveaux EULA de votre choix et relancer votre toutim!
Lien vers l'original

# Cours 4

Banque de cours - Quelques magnifiques Dataviz

# Quelques magnifiques Dataviz

Lien vers l'original

Banque de cours - Lecture CSV via Geo Nodes

Lecture CSV via Geo Nodes

Pour le CSV, c’est par ici (Merci à MTR Animation d’avoir rendu disponible cette donnée): https://drive.google.com/file/d/1v2oEnUkhGDECPXWKIdZmJMWYEcyljtUX/view?usp=sharing

Lien vers l'original

Dataviz Jam I - EULA - Suite! On snuggyfie tout ça!

  • Vous pouvez mélanger les équipes, vous entraider, vous inspirer des idées des autres

# Cours 5

Banque de cours - Quelques autres magnifiques Dataviz

# Quelques autres magnifiques Dataviz

Lien vers l'original

Banque de cours - Major cities population mapped onto Earth - Partie 1

Major cities population mapped onto Earth - Partie 1

☝🏼

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:


  • 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!
Lien vers l'original

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.

🔹 Utilisation typique :

Mélanger deux shaders :

  • Un Diffuse BSDF (vue de face)
  • Un Glossy BSDF (vue de biais)

Exemple :

Fresnel (IOR=1.45)

└──> Mix Shader (Fac=Fresnel)
     ├── Diffuse BSDF
     └── Glossy BSDF

→ Résultat : un matériau réaliste dont les reflets augmentent selon l’angle de vue.


Le Layer Weight Node

🔹 Localisation :

Shader Editor → Add → Input → Layer Weight

🔹 Fonction :

Ce nœud est plus artistique et flexible.

Il ne se limite pas à la physique optique (comme Fresnel) mais te donne deux types de courbes liées à l’angle de vue :

  • Fresnel
  • Facing

🌀 Sortie :

Fresnel

Similaire au Fresnel Node, mais moins physiquement exact — c’est une approximation pratique.

  • Blend (entrée) : contrôle la douceur de la transition.
    • 0 → transition brutale
    • 1 → transition très douce
  • Fresnel (sortie) : valeur entre 0 et 1, plus élevée sur les bords (angles rasants).

déal pour créer des effets stylisés ou ajuster le contraste visuel sans se soucier des valeurs physiques exactes d’IOR.


Sortie :

Facing

C’est le complément du Fresnel :

  • 1 quand la surface fait face à la caméra.
  • 0 quand elle est vue de biais.

En gros :

“Facing” =
vue de face

“Fresnel” =
vue de biais


Utilisation typique :

  • Mélanger des shaders selon l’angle :
    → “Facing” pour un matériau mat au centre, brillant sur les bords.
  • Contrôler un dégradé sur la surface.
  • Animer des effets comme des lueurs sur les bords (rim light).

Exemple :

Layer Weight (Blend=0.3)

├── Fresnel → Mix Shader (brillance sur les bords)
└── Facing  → Factor pour un ramp color (effet halo)

4. Différences résumées

CaractéristiqueFresnel NodeLayer Weight Node
But principalPhysiquement exact (IOR)Contrôle artistique flexible
Entrée principaleIORBlend
Sortie principaleFresnel (angle de réflexion)Fresnel et Facing
Valeur de sortie0–1 selon l’angle0–1 selon angle ou face
Idéal pourRéalisme (verre, eau, plastique)Stylisation (halo, rim light, toon shading)

5. Exemples d’usage

Matériau réaliste

Fresnel (IOR=1.45)
 → Mix Shader
    ├── Diffuse BSDF
    └── Glossy BSDF

→ Reflète plus sur les bords, parfait pour plastique ou verre.


Effet artistique type “rim light”

Layer Weight (Blend=0.3)
 → Facing → ColorRamp → Emission Strength

→ 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.

Lien vers l'original

  • 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!

Lien vers l'original

# Cours 8

Banque de cours - AMazeIng

AMazeIng

Que fait ce bout de code selon vous? Et en quel langage est-il écrit?

char*M,A,Z,E=40,J[40],T[40];main(C){for(*J=A=scanf(M="%d",&C);
--            E;             J[              E]             =T
[E   ]=  E)   printf("._");  for(;(A-=Z=!Z)  ||  (printf("\n|"
)    ,   A    =              39              ,C             --
)    ;   Z    ||    printf   (M   ))M[Z]=Z[A-(E   =A[J-Z])&&!C
&    A   ==             T[                                  A]
|6<<27<rand()||!C&!Z?J[T[E]=T[A]]=E,J[T[A]=A-Z]=A,"_.":" |"];}

On a Maze program - The art of obfuscation par John Tromp

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 2
 
typedef 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...

…Rickperry@vill.edu == r.perry@ieee.org

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
 

Highlight of the cells:

           0    1    2    3    4    5    6    7
       XX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|
       --|----|----|----|----|----|----|----|----|
     0 XX|  XX|    |    |    |  XX|    |    |  XX|
       XX|  XX|  XX|XXXX|  XX|XXXX|  XX|XXXX|  XX|
       --|----|----|----|----|----|----|----|----|
     1 XX|  XX|  XX|    |    |    |    |  XX|    |
       XX|  XX|XXXX|XXXX|XXXX|  XX|XXXX|XXXX|  XX|
       --|----|----|----|----|----|----|----|----|
     2   |    |  XX|  XX|    |    |    |  XX|  XX|
       XX|  XX|XXXX|  XX|XXXX|  XX|  XX|  XX|  XX|
       --|----|----|----|----|----|----|----|----|
     3 XX|    |    |    |    |  XX|  XX|  XX|  XX|
       XX|  XX|  XX|XXXX|  XX|XXXX|  XX|  XX|XXXX|
       --|----|----|----|----|----|----|----|----|
     4 XX|  XX|  XX|    |    |  XX|  XX|    |  XX|
       XX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|XXXX|
       --|----|----|----|----|----|----|----|----|
 

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   1
 
typedef 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:

cell[i][j].kind = FRONTIER;
 
cell[i][j].down = OPEN;
 
if( cell[i][j].kind == NEITHER) ...
 

The algorithm to create a maze is:

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.

     0   1   2   3
   ++++++++++++++++++
 0 ++  ++  ++F ++  ++	Cell (1,2) chosen as the initial spanning tree.
   ++++++++++++++++++
 1 ++  ++F ++S ++F ++	Adjacent cells (0,2), (1,1), (1,3), and (2,2)
   ++++++++++++++++++
 2 ++  ++  ++F ++  ++	added to frontier list.
   ++++++++++++++++++
 3 ++  ++  ++  ++  ++
   ++++++++++++++++++
 4 ++  ++  ++  ++  ++
   ++++++++++++++++++
 
     0   1   2   3
   ++++++++++++++++++
 0 ++  ++  ++F ++  ++
   ++++++++++++++++++
 1 ++  ++F ++S ++F ++
   ++++++++++  ++++++
 2 ++  ++F ++S ++F ++	Frontier cell (2,2) added to the spanning tree.
   ++++++++++++++++++
 3 ++  ++  ++F ++  ++	Adjacent cells (2,1), (2,3) and (3,2)
   ++++++++++++++++++
 4 ++  ++  ++  ++  ++	added to the frontier list.
   ++++++++++++++++++
 
     0   1   2   3
   ++++++++++++++++++
 0 ++  ++  ++F ++  ++
   ++++++++++++++++++
 1 ++  ++F ++S ++F ++
   ++++++++++  ++++++
 2 ++F ++S   S ++F ++	Frontier cell (1,2) added to the spanning tree.
   ++++++++++++++++++
 3 ++  ++F ++F ++  ++	Adjacent cells (0,2) and (1,3) added to the
   ++++++++++++++++++
 4 ++  ++  ++  ++  ++	frontier list.
   ++++++++++++++++++
 

L'Algorithme de Backtracking Récursif

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

  1. 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))
  2. Exploration
    • Marquer la cellule courante comme “visitée”
    • Obtenir la liste des cellules voisines non visitées
  3. 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
  4. 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 que
toutes 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:

  1. Élégance conceptuelle - L’idée de “creuser” des tunnels en explorant puis en revenant en arrière est intuitive
  2. Simplicité d’implémentation - Quelques dizaines de lignes suffisent
  3. Qualité esthétique - Produit des labyrinthes avec de longs passages sinueux, visuellement satisfaisants
  4. 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:

  1. Pile (Stack):
    • Stocke le chemin parcouru
    • Permet de revenir en arrière (backtracking)
    • Structure LIFO (Last In, First Out)
  2. Boucle principale:
    • Continue tant qu’il reste des cellules dans la pile
    • Gère deux cas: exploration et backtracking
  3. 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:

  1. Collecter tous les murs du périmètre
  2. Mélanger aléatoirement la liste
  3. 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_size
pos_y = y × cell_size
 
Exemple: 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:

  1. Création d’un cube avec primitive_cube_add
  2. Positionnement au centre du mur
  3. Mise à l’échelle pour obtenir les bonnes dimensions
  4. Application du matériau bleu
  5. 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)

c) Création des Marqueurs (_create_markers)

def _create_markers(self, collection):
    # Marqueur d'entrée (vert)
    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"
 
    # Matériau vert avec émission
    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
 

Matériaux avec émission:

  • Base Color: Couleur de base de l’objet
  • Emission Color: Couleur de la lumière émise
  • 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
)
 

Exemples de Configuration

Labyrinthe Compact

# Petit labyrinthe, murs fins, basse hauteur
maze = MazeGenerator(width=8, height=8, wall_thickness=0.1,
                     wall_height=2.0, cell_size=1.5)
 
  • Idéal pour: tests rapides, visualisation simple
  • Temps de génération: < 1 seconde
  • Dimensions finales: 12 × 12 unités Blender

Labyrinthe Épique

# Grand labyrinthe, murs épais, haute hauteur
maze = MazeGenerator(width=50, height=50, wall_thickness=0.3,
                     wall_height=5.0, cell_size=3.0)
 
  • Idéal pour: jeux vidéo, explorations complexes
  • Temps de génération: quelques secondes
  • Dimensions finales: 150 × 150 unités Blender

Labyrinthe Artistique

# Proportions asymétriques
maze = MazeGenerator(width=30, height=15, wall_thickness=0.15,
                     wall_height=4.0, cell_size=2.5)
 
  • Idéal pour: designs uniques, compositions visuelles
  • Forme rectangulaire plutôt que carrée

Modifications Avancées

1. Changer les Couleurs

Murs:

# Dans create_maze_geometry(), ligne créant le matériau
mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (R, G, B, 1.0)
 
# Exemples:
# Murs rouges: (1.0, 0.0, 0.0, 1.0)
# Murs dorés: (1.0, 0.84, 0.0, 1.0)
# Murs noirs: (0.0, 0.0, 0.0, 1.0)
 

Marqueurs:

# Entrée en bleu au lieu de vert
bsdf.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ée
bpy.ops.mesh.primitive_uv_sphere_add(
    radius=0.5,
    location=(entrance_pos[0], entrance_pos[1], 0.5)
)
 
# Cône pour la sortie
bpy.ops.mesh.primitive_cone_add(
    radius1=0.5,
    depth=1.0,
    location=(exit_pos[0], exit_pos[1], 0.5)
)
 

4. Ajouter de l’Éclairage

def _create_lighting(self):
    """Ajouter une lumière au-dessus du labyrinthe."""
    bpy.ops.object.light_add(
        type='SUN',
        location=(self.width * self.cell_size / 2,
                 self.height * self.cell_size / 2,
                 self.wall_height + 10)
    )
    light = bpy.context.active_object
    light.data.energy = 5.0
 

Extensions Possibles

Algorithmes Alternatifs

  1. Algorithme de Prim (Croissance)
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
 
  1. 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

  1. 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
 
  1. 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)
 
  1. 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:

  1. 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 objets
instance = template_wall.copy()
instance.data = template_wall.data  # Partage la géométrie
instance.location = (x, y, z)
 
  1. Fusion des murs adjacents:
# Au lieu de créer un mur par segment,
# fusionner les murs alignés en un seul mesh
 
  1. 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 principal
maze = MazeGenerator(20, 20)
maze.generate_maze()
maze.create_entrance_exit()
maze.create_maze_geometry("MyMaze")
 
# Accéder aux données
entrance_cell = maze.entrance
print(f"Entrée en position: {entrance_cell}")
 

Génération Procédurale dans un Jeu

# Générer un nouveau niveau à chaque partie
import time
 
seed = 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

  1. Algorithmes de Labyrinthes:
    • “Mazes for Programmers” par Jamis Buck
    • “Think Labyrinth!” par Walter D. Pullen
  2. Backtracking et Graphes:
    • “Introduction to Algorithms” par Cormen et al. (CLRS)
    • “The Algorithm Design Manual” par Steven Skiena
  3. 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 Generator
A Python script to generate 3D mazes in Blender using recursive backtracking algorithm.
"""
 
import bpy
import random
 
 
class 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 execution
if __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 Generator
A Python script to generate 3D mazes in Unreal Engine using recursive backtracking algorithm.
Requires: Unreal Engine with Python Editor Script Plugin enabled
"""
 
import unreal
import random
 
 
class 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 execution
if __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()")

Dans UE:

  • Activer Python dans Unreal: Edit → Plugins → “Python Editor Script Plugin”
  • Exécuter via: Tools → Execute Python Script
  • Personnaliser les paramètres (width, height, wall_height, etc.)

À vous de jouer, ajoutez le pack First Person et trouvez la sortie du labyrinthe!

Lien vers l'original

Banque de cours - City in Blender using Google 3D Cities or OpenStreetMap

City in Blender using Google 3D Cities or OpenStreetMap

Téléchargez l’addon Blosm for Blender: https://prochitecture.gumroad.com/l/blender-osm

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:

Lien vers l'original

# Cours 9

# Cours 10

  • TouchDesigner I
  • Projet intégrateur - Vélos et Montréal

# Cours 11

  • TouchDesigner II
  • Projet intégrateur - Vélos et Montréal

# Cours 12

  • TouchDesigner III
  • Projet intégrateur - Vélos et Montréal

# Cours 13

  • Pop Quiz !
    • Considérez l’IRM d’une tête d’humain suivant:
    • Dépatouillez-vous pour le visualiser !
      • Logiciels autorisés : Blender, Unreal, Unity, Maya, 3DS Max, Houdini
      • Vous avez le droit à tout (I.A., vos collègues, moi, etc.)
      • Vous avez les 3h - moins la pause bien sûr !
      • Bonus : un « driver » qui permet de parcourir/faire apparaître les slices, comme si on scannait en temps réel.
    • À remettre:
      • Un ou des visuels du résultat. Une vidéo si vous avez réussi à simuler un scan par tranches
      • Un ficher README.md expliquant la marche à suivre pour obtenir votre résultat
      • Conséquemment, les scripts et outils utilisés (web, etc.) doivent êtres fournis
      • Dans le readme, ajoutez un ou deux paragraphes expliquant votre démarche, stratégie, vos frustrations, vos satisfactions!
      • Bonne chance!

# Cours 14

Biomédical - visualisation d’un IRM

  • Un peu de théorie sur les scans médicaux
  • Projet intégrateur - Vélos et Montréal

# Cours 15

  • Projet intégrateur - Vélos et Montréal