Blender
Un modèle pilote Blender 4.3.2 par le Hub pour construire une scène à partir de celle par défaut et la rendre en PNG. Cette page donne le texte, le script, les réponses du Hub et le rendu, avec l’erreur de couleur qu’une réponse indiquant un succès a cachée, et les 30 secondes du premier rendu.
Sur cette page
Démarrer Blender avec son pont
Blender fait tourner le pont dans son propre Python. Démarré en mode arrière-plan, il quitte dès que le fichier du pont a fini de s’exécuter ; un fichier de démarrage donne donc le fil principal au pont avec serve_main_thread(), comme le décrit le guide. L’exécution n’avait aucune variable DISPLAY.
blender --background --python boot.py
import runpy
ns = runpy.run_path('/usr/share/libremodular-mcp/bridges/blender_bridge.py')
ns['serve_main_thread']()
L’exécution a utilisé le port 9313 dans config.toml et un jeton neuf. Elle a aussi réglé default_timeout_s = 120 dans la section [hub], parce que le premier rendu d’un profil neuf est lent et que le délai par défaut est de 30 s (voir la réponse plus bas). Le port s’est mis à l’écoute 1,3 s après le démarrage, et get_state sur hub a listé Blender comme connecté :
{
"app": "hub",
"app_version": "0.1.0",
"bridge_version": null,
"captured_at": "2026-10-21T12:47:58.570Z",
"connected": true,
"content_origin": "hub",
"screenshot": null,
"state": {
"modules": [
{
"app_version": "4.3.2",
"bridge_version": "1",
"connected": true,
"mode": "tcp",
"name": "blender"
}
]
},
"warnings": []
}Texte donné au modèle
Avant d’écrire, le modèle a appelé get_state sur blender : la scène par défaut, avec les objets Cube, Light et Camera, le moteur BLENDER_EEVEE_NEXT et les images d’animation 1 à 250. Il a écrit son script pour ce qui s’y trouvait : il a réutilisé le cube et la lumière, et gardé la caméra.
Script écrit
Un modèle, Claude Sonnet 5.5, a écrit ce script à partir du guide et de cet état. Ce n’est pas un script tiré des tests du projet. Les couleurs sont converties de sRGB en linéaire avant d’arriver au matériau et au monde, parce que Blender les stocke en linéaire.
import os
import bpy
def to_linear(value):
return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4
def colour(code):
red, green, blue = (int(code[i:i + 2], 16) / 255 for i in (1, 3, 5))
return (to_linear(red), to_linear(green), to_linear(blue), 1.0)
def material(name, code):
item = bpy.data.materials.new(name)
item.use_nodes = True
item.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = colour(code)
return item
scene = bpy.context.scene
cube = bpy.data.objects["Cube"]
cube.data.materials.clear()
cube.data.materials.append(material("Copper", "#c25438"))
ground_mesh = bpy.data.meshes.new("Ground")
ground_mesh.from_pydata([(-6, -6, -1), (6, -6, -1), (6, 6, -1), (-6, 6, -1)], [], [(0, 1, 2, 3)])
ground_mesh.update()
ground = bpy.data.objects.new("Ground", ground_mesh)
scene.collection.objects.link(ground)
ground.data.materials.append(material("Grey", "#d8d4cf"))
light = bpy.data.objects["Light"]
light.data.type = "SUN"
light.data.energy = 4.0
light.rotation_euler = (0.9, 0.2, 0.6)
world = scene.world or bpy.data.worlds.new("World")
scene.world = world
world.use_nodes = True
world.node_tree.nodes["Background"].inputs["Color"].default_value = colour("#1c385c")
scene.render.resolution_x = 640
scene.render.resolution_y = 360
scene.render.filepath = os.path.join(LMM_WORK_DIR, "render.png")
bpy.ops.render.render(write_still=True)
result = {"objects": [item.name for item in scene.objects], "png_bytes": os.path.getsize(scene.render.filepath)}La réponse du Hub
{
"app": "blender",
"duration_ms": 29947,
"error": null,
"result": {
"objects": [
"Ground",
"Cube",
"Light",
"Camera"
],
"png_bytes": 216713
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}status vaut ok, et duration_ms vaut 29 947 : le premier rendu dans un profil neuf a pris 30 s, comme le dit le guide. Le rendu suivant, dans le même Blender, a pris 149 ms (plus bas). Avec le délai par défaut du Hub, qui est de 30 s, le même appel peut se terminer par timeout, comme le consigne la page de compatibilité.
Ce que le premier rendu a montré
La réponse ne dit pas si les couleurs sont justes. Lu dans le PNG, le pixel en haut à gauche, qui est le monde, valait #103759, alors que le script avait réglé #1c385c. Un second script a lu la gestion des couleurs de la scène :
import bpy
view = bpy.context.scene.view_settings
result = {"view_transform": view.view_transform, "look": view.look, "exposure": view.exposure, "gamma": view.gamma, "engine": bpy.context.scene.render.engine}{
"app": "blender",
"duration_ms": 0,
"error": null,
"result": {
"engine": "BLENDER_EEVEE_NEXT",
"exposure": 0.0,
"gamma": 1.0,
"look": "None",
"view_transform": "AgX"
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}La scène utilise la transformation de vue AgX, valeur par défaut de ce Blender, qui change les couleurs sur le chemin du PNG. La transformation Standard laisse une couleur unie telle quelle.
Le script corrigé et sa réponse
Seul ce qui a échoué a changé : le script règle la transformation de vue sur Standard et rend de nouveau.
import os
import bpy
scene = bpy.context.scene
scene.view_settings.view_transform = "Standard"
scene.render.filepath = os.path.join(LMM_WORK_DIR, "render.png")
bpy.ops.render.render(write_still=True)
result = {"view_transform": scene.view_settings.view_transform, "png_bytes": os.path.getsize(scene.render.filepath)}{
"app": "blender",
"duration_ms": 149,
"error": null,
"result": {
"png_bytes": 210434,
"view_transform": "Standard"
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}Le pixel en haut à gauche vaut maintenant #1C385C, la couleur demandée.
L’image produite
Voici le rendu, de 640 par 360 pixels. Le cube et le sol sont éclairés par le soleil, de sorte que leurs pixels ne sont pas les couleurs de leurs matériaux : le dessus du cube vaut #A84A39 pour un matériau réglé sur #c25438, et le sol vaut #BEBEC1 pour #d8d4cf.
