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

*La scène du logo, construite et rendue par le Hub dans Blender : une coupe passe du rendu au maillage en fil de fer pendant que la caméra tourne autour du noyau.*

## Faits

- Version essayée : 4.3.2
- Mode : `tcp`

## 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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md). L'exécution n'avait aucune variable `DISPLAY`.

```sh
blender --background --python boot.py
```

```python
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é :

**Réponse du Hub**

```json
{
  "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

```text
In Blender, build a small scene from what is already open and render it: the cube in copper (#c25438), a light grey (#d8d4cf) ground just under it, the light turned into a sun, a dark blue (#1c385c) background. Render 640 by 360 pixels and write render.png in your working directory.
```


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.

**Script écrit**

```python
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

**Réponse du Hub**

```json
{
  "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é](https://libremodular.davalan.fr/fr/compatibility/index.md#blender).

## 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 :

**Script écrit**

```python
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}
```


**Réponse du Hub**

```json
{
  "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.

**Script écrit**

```python
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)}
```


**Réponse du Hub**

```json
{
  "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`.

![Un cube cuivre sur un sol gris clair devant un fond bleu foncé.](https://libremodular.davalan.fr/images/applications/blender-cube.png)

*render.png après le script corrigé, rendu dans EEVEE avec la transformation de vue Standard.*


> **Limite**
>
> Non essayé : démarrer le pont depuis la console de l'espace de travail Scripting d'un Blender ouvert, un vrai écran, des opérateurs de Blender autres que l'ajout d'objets et le rendu, et le moteur Workbench, dont le [guide](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md) liste les pièges de couleur. La transformation de vue n'a été mesurée que dans le moteur par défaut.

[Guide de référence dans le dépôt](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md)

