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.
En esta página
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. La ejecución no tenía la 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']()
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:
{
"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
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.
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
{
"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.
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:
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 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.
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
}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.
