Le Lab · Carnet nº 1

Piloter Blender par le code.

Un décor de nuit niveau cinéma, construit et éclairé par un agent IA dans Blender headless, puis cuit en 6,6 Mo pour le navigateur.

20 juillet 2026 · 7 min de lecture · Python (bpy) · Cycles · three.js
Rendu 3D d'un bureau de nuit : lampe d'architecte allumée, ordinateur rétro, fauteuil en cuir, post-its au mur
Rendu Cycles brut, sans retouche. 1080p, 256 échantillons, sur un MacBook M1. Personne n'a ouvert l'interface de Blender.

Personne n'a modelé ce bureau. Pas de souris, pas de viewport, pas d'artiste : des scripts Python écrits par un agent IA, exécutés par Blender en ligne de commande. Le même code a ensuite compressé le décor entier en 6,6 Mo pour qu'il tourne dans une page web. On déroule le carnet, avec le vrai code.

Le problème

Nos expériences web réclament des décors qui tiennent à côté d'un plan de film : lumière douce, bazar crédible, profondeur de champ. Sauf que ces décors doivent tourner à 60 images par seconde sur un téléphone. Cycles met plusieurs minutes à calculer une image ; le navigateur, lui, dispose de 16 millisecondes. Les deux mondes ne se parlent pas.

Autre donnée du problème : pas d'artiste 3D dans l'équipe. Le studio, c'est un humain qui décide et un agent IA qui exécute. Blender n'allait donc pas être ouvert. Il allait être piloté.

Blender sans interface

Blender embarque un interpréteur Python complet, bpy, et accepte très bien de tourner sans interface. La scène entière tient donc dans un programme :

/Applications/Blender.app/Contents/MacOS/Blender \
  --background --python build_office.py -- --render

Pendant la construction, l'agent ne voit rien. Il décrit, en mètres. La pièce fait 4,2 m sur 5,2 m : des boîtes, tout bêtement, jusqu'au mur de droite qu'il a fallu découper en quatre panneaux pour y percer la fenêtre :

box('floor',  (4.2, 5.2, 0.05), (0, 0, -0.025), WOOD)
box('wall_back', (4.2, 0.06, 3.0), (0, -2.6, 1.5), WALL)
# Mur droit PERCÉ d'une fenêtre (ouverture y -1.35..0.15, z 0.85..2.5)
box('wall_right_a', (0.06, 5.2, 0.85), (2.1, 0, 0.425), WALL)  # sous la fenêtre
box('wall_right_b', (0.06, 5.2, 0.5),  (2.1, 0, 2.75),  WALL)  # au-dessus

Le mobilier n'a pas été modélisé. Tout vient de Poly Haven, une bibliothèque d'objets scannés en licence CC0 qu'on interroge par API : le bureau métallique, le fauteuil, la lampe articulée, le téléviseur CRT, le laptop des années 90. Une seule fonction, place(), importe chaque objet, le mesure, le remet à l'échelle et le pose au sol. Le raisonnement d'un décorateur : ce bureau fait 78 cm, il va là, tourné comme ça.

desk  = place(import_asset('polyhaven/desk', 'desk'), target_h=0.78, loc=(-0.5, -1.85, 0))
chair = place(import_asset('polyhaven/office-chair', 'chair'), target_h=0.95,
              loc=(-1.05, -0.8, 0), rot_z=math.radians(210))
crt   = place(import_asset('polyhaven/crt-tv', 'crt'), target_h=0.42,
              loc=(0.15, -2.1, DESK_TOP), rot_z=math.radians(42))

Le désordre aussi est du code

Un bureau rangé ne raconte rien ; celui-ci avait besoin d'un passé. Le module clutter.py n'importe aucun asset. Des primitives et des règles, c'est tout. Piles de A4 jamais tout à fait droites, dossiers dépareillés, câbles qui pendent en caténaire, quatorze post-its. Chaque pile a sa graine aléatoire et son paramètre messiness : à 0 la pile est nette, à 1 le commentaire du script parle de « bazar de fin de nuit » :

clutter.paper_stack((-1.0, -1.55, DESK_TOP), height=0.16, seed=3, messiness=1.0)
clutter.folder_stack((1.1, -2.2, DESK_TOP), count=7, seed=5)
clutter.cable((0.15, -2.3, DESK_TOP), (0.9, -2.5, 0.05), sag=0.25, seed=11)
clutter.postits((-0.1, -2.555, 1.35), 'y', count=14, seed=9)  # collés au mur

La lumière fait le film

Quatre sources. La lampe de bureau, chaude, qui porte toute la scène. Une lumière de ville, froide, à travers les stores vénitiens. Les écrans. Et un tiroir resté entrouvert, d'où fuit une lumière dorée. Ce qu'il contient ne se dira pas dans ce carnet.

key.data.energy = 180
key.data.color  = (1.0, 0.78, 0.5)          # la lampe : chaude, la clé
moon.data.color = (0.6, 0.71, 0.95)         # la ville : froide, par les stores
box('drawer_gold', ..., emit=(1.0, 0.72, 0.28), strength=90)   # le tiroir
scene.view_settings.view_transform = 'AgX'  # + look 'AgX - Punchy'

Le tout passe par la transformation de couleur AgX, celle qui roule doucement les hautes lumières au lieu de les cramer en blanc, comme le faisaient les rendus 3D d'il y a dix ans.

L'agent se relit

Le premier rendu n'était pas bon.

Première version du rendu : l'écran du téléviseur CRT est un rectangle blanc surexposé face à la caméra
Version 1. L'écran CRT : un rectangle blanc cramé, plein axe caméra.
Version corrigée : le téléviseur est tourné vers le fauteuil, sa lueur est bleutée et calibrée
Version 2. Le poste pivoté de 42° vers le fauteuil, lueur bleutée, émission calibrée.

Personne n'est intervenu entre ces deux images. L'agent a rendu la scène, regardé le résultat, remarqué l'écran surexposé qui fixait la caméra, puis corrigé son propre script : le poste pivote vers le fauteuil, l'émission baisse, la teinte refroidit. La correction qu'un chef opérateur ferait à l'œil, faite ici en relisant du code.

Les chiffres, sur un simple MacBook M1

Le pont vers le navigateur : cuire la lumière

Aucun téléphone ne calculera cet éclairage en direct, et il n'en a pas besoin. L'astuce est vieille comme le jeu vidéo : calculer la lumière une fois, puis la peindre sur les murs. C'est le bake.

Le second script fusionne la scène en deux objets, la coquille (murs, sol, plafond, stores) et le contenu (meubles, désordre), déplie chacun sur un atlas UV neuf et cuit l'éclairage Cycles dans deux textures. Puis il remplace les matériaux. Le fichier exporté ne contient plus une seule lampe : la lumière est devenue la texture. Restent émissifs les écrans et l'or du tiroir, qui continuent de vivre en temps réel :

groups = {'shell': murs_sol_plafond, 'stuff': tout_le_reste}
bpy.ops.uv.smart_project(island_margin=0.002)
bpy.ops.uv.pack_islands(margin=0.003)   # sinon les îlots s'entassent dans un coin
bpy.ops.object.bake(type='COMBINED', margin=8, use_clear=True)
bpy.ops.export_scene.gltf(filepath='office_baked.glb', export_format='GLB')

Trois pièges rencontrés en chemin :

1. L'image cuite doit être sauvée manuellement par script : la sauvegarde automatique du bake est cassée en headless.
2. Sans pack_islands après l'unwrap, tous les îlots UV s'entassent dans un coin de l'atlas.
3. Sur M1, un bake GPU peut sortir des artefacts : le même bake refait en CPU les élimine.

Au bout de la chaîne : un GLB de 6,6 Mo que three.js charge comme n'importe quel modèle. Sauf que celui-là embarque un éclairage de cinéma qui ne coûte plus rien à calculer.

Ce que ça change, aussi pour votre entreprise

Ce carnet ne cherche pas à vendre de la 3D. La 3D est notre banc d'essai, le plus exigeant qu'on ait trouvé : géométrie, lumière et budget de performance doivent être justes en même temps. Ce qui compte, c'est la mécanique en dessous : un agent IA sait piloter n'importe quel logiciel qui s'automatise par code. Il ne clique pas plus vite qu'un humain. Il écrit, il exécute, il lit le résultat, il se corrige.

Mettez votre ERP, votre outil de planification ou votre chaîne documentaire à la place de Blender : la boucle ne change pas. C'est celle que nous installons chez nos clients.

Parlons de votre entreprise
© 2026 The Good Life Studio From Belgium · hello@thegoodlife.studio