# Blender

Un modelo maneja Blender 4.3.2 a través del Hub para construir una escena a partir de la predeterminada y renderizarla a un PNG. Esta página da el texto, el script, las respuestas del Hub y el renderizado, con el error de color que ocultó una respuesta satisfactoria y los 30 segundos del primer renderizado.

*La escena del logotipo, construida y renderizada por el Hub en Blender: un corte pasa del renderizado a la malla de alambre mientras la cámara gira alrededor del núcleo.*

## Datos

- Versión probada: 4.3.2
- Modo: `tcp`

## Iniciar Blender con su puente

Blender ejecuta el puente en su propio Python. Iniciado en segundo plano, termina en cuanto se ha ejecutado el archivo del puente, así que un archivo de arranque entrega el hilo principal al puente con `serve_main_thread()`, como describe la [guía](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md). La ejecución no tenía la 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']()
```

La ejecución usó el puerto 9313 en `config.toml` y un token nuevo. También fijó `default_timeout_s = 120` en la sección `[hub]`, porque el primer renderizado de un perfil nuevo es lento y el plazo predeterminado es de 30 s (véase la respuesta de más abajo). El puerto empezó a escuchar 1,3 s después del inicio, y `get_state` sobre `hub` enumeró Blender como conectado:

**Respuesta del 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": []
}
```


## Texto dado al modelo

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


Antes de escribir, el modelo llamó a `get_state` sobre `blender`: la escena predeterminada, con los objetos `Cube`, `Light` y `Camera`, el motor `BLENDER_EEVEE_NEXT` y los fotogramas de 1 a 250. Escribió su script para lo que había: reutilizó el cubo y la luz, y conservó la cámara.

## Script escrito

Un modelo, Claude Sonnet 5.5, escribió este script a partir de la guía y de ese estado. No es un script tomado de las pruebas del proyecto. Los colores se convierten de sRGB a lineal antes de llegar al material y al mundo, porque Blender los almacena en lineal.

**Script escrito**

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


## Respuesta del Hub

**Respuesta del 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` es `ok`, y `duration_ms` vale 29 947: el primer renderizado en un perfil nuevo tardó 30 s, como dice la guía. El renderizado siguiente, en el mismo Blender, tardó 149 ms (más abajo). Con el plazo predeterminado del Hub de 30 s, la misma llamada puede acabar en cambio en `timeout`, como registra la [página de compatibilidad](https://libremodular.davalan.fr/es/compatibility/index.md#blender).

## Lo que mostró el primer renderizado

La respuesta no dice si los colores son correctos. Leído en el PNG, el píxel de arriba a la izquierda, que es el mundo, era `#103759`, donde el script había fijado `#1c385c`. Un segundo script leyó la gestión del color de la escena:

**Script escrito**

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


**Respuesta del 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 escena usa la transformación de vista `AgX`, la predeterminada de este Blender, que cambia los colores en el camino hacia el PNG. La transformación `Standard` deja un color uniforme tal como está.

## El script corregido y su respuesta

Solo cambió lo que falló: el script fija la transformación de vista en `Standard` y renderiza de nuevo.

**Script escrito**

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


**Respuesta del Hub**

```json
{
  "app": "blender",
  "duration_ms": 149,
  "error": null,
  "result": {
    "png_bytes": 210434,
    "view_transform": "Standard"
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}
```


El píxel de arriba a la izquierda se lee ahora `#1C385C`, el color que se había pedido.

## La imagen producida

Este es el renderizado, de 640 por 360 píxeles. El cubo y el suelo están iluminados por el sol, así que sus píxeles no son los colores de sus materiales: la cara superior del cubo se lee `#A84A39` para un material fijado en `#c25438`, y el suelo se lee `#BEBEC1` para `#d8d4cf`.

![Un cubo de cobre sobre un suelo gris claro frente a un fondo azul oscuro.](https://libremodular.davalan.fr/images/applications/blender-cube.png)

*render.png tras el script corregido, renderizado en EEVEE con la transformación de vista Standard.*


> **Límite**
>
> No se probó: iniciar el puente desde la consola del espacio de trabajo Scripting de un Blender abierto, una pantalla real, operadores de Blender distintos de añadir objetos y renderizar, y el motor Workbench, cuyas trampas de color enumera la [guía](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md). La transformación de vista se midió solo en el motor predeterminado.

[Guía de referencia en el repositorio](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/modules/blender.md)

