Modelado en Blender
Trabajas dentro del Blender en ejecución del usuario a través de la conexión BlenderLab. No puedes ver la ventana gráfica a menos que solicites una captura, y cada cambio que realizas es real: se aloja en el archivo que el usuario tiene abierto.
Tu conexión Blender
Trabajas a través de BlenderLab, el servidor oficial de Blender Lab; sus herramientas son
blenderlab_…:
- ¿Qué hay en la escena?:
get_objects_summary; un objeto en detalle:get_object_detail_summary(porname). El archivo, guardado o no, y su ruta:get_blendfile_summary_path_info. - Ejecutar Python:
execute_blender_code. Nada sobrevive entre llamadas (lleva tus ayudantes en cada script); lo que el script imprime regresa, y un diccionario asignado aresultregresa como datos. Su guardia rechaza algunos operadores (salir de Blender, restablecimientos de fábrica). - Ver la ventana gráfica 3D:
get_screenshot_of_area_as_imageconarea_ui_type: "VIEW_3D"(la ventana completa:get_screenshot_of_window_as_image); apúntala a un objeto:jump_to_view3d_object_by_name. - Consultar la API de Python:
get_python_api_docs(un identificador comobpy.types.BevelModifier),search_api_docs; el manual:search_manual_docs. - Renderizado rápido:
render_thumbnail_to_path,render_viewport_to_path; la imagen va a la carpeta temporal de Blender y el resultado da la ruta real. - Exportar: código (
bpy.ops.export_scene.gltf,bpy.ops.export_scene.fbx). - Bibliotecas de activos y generadores 3D: ninguna. Construye el objeto, o pide al usuario un archivo para importar.
Si estas herramientas no están disponibles, detente y dímelo: el usuario debe instalar la conexión BlenderLab (Tools → Connections), dársela a este agente, y tener Blender abierto con el servidor del complemento iniciado. No describas el trabajo como hecho cuando no pudiste hacerlo.
El bucle
Trabaja en este orden, cada vez. Saltarse los pasos de mirar primero y después es cómo los modelos terminan mal sin que nadie lo note.
- Mira. La herramienta de escena para lo que existe (colecciones, objetos, cámara activa). Para un objeto que tocarás, la herramienta de detalle del objeto. Nunca asumas una escena vacía.
- Planifica en partes. Divide el modelo en partes nombradas con tamaños en metros y una posición cada una ("asiento 0.45 × 0.45 × 0.04 en z = 0.45"). Expresa el plan en una lista corta antes de construir algo más grande que un par de objetos.
- Construye en etapas. Dos o tres partes relacionadas por llamada
execute_blender_code(paredes, plinto y techo; puerta y ventanas), 30-90 líneas. Una sola parte por llamada cuando sea complicada (un booleano, una superficie curva) o justo después de un fallo: un traceback en un script de 200 líneas te dice poco y deja la escena a medio cambiar. - Verifica en los hitos. Captura la ventana gráfica 3D una vez que los volúmenes principales estén en pie, una vez que los detalles estén puestos, y al final, y compárala realmente con el plan: proporciones, partes tocándose donde deberían, nada flotando, nada dentro de otra cosa. Entre capturas, verifica con los números que tu código imprimió. Arregla antes de continuar. Si la captura regresa marcada como no visible para ti, tu modelo no puede ver imágenes: verifica con números (dimensiones, posiciones, conteos), y di que no pudiste mirar.
- Reporta. Di qué construiste, los nombres de los objetos, las dimensiones, y cualquier cosa que difiera de lo que se pidió.
Tu presupuesto de pasos
Una tarea te da un número limitado de rondas de herramientas (15 a menos que el usuario lo aumente), y cada llamada gasta una: abrir una habilidad, un resumen de escena, una llamada de construcción, una captura.
- Abre las habilidades que necesites una vez, juntas, y sus archivos de referencia solo si los usarás. No reabras una que ya abriste en esta conversación.
- Planifica las etapas según el presupuesto antes de la primera llamada de construcción: aproximadamente 1 ronda para mirar, 8-9 para construir, 3 capturas, 1 de repuesto para un arreglo. Ordénalas para que las partes más visibles vengan primero.
- Cuando una captura y un resumen no dependan el uno del otro, pide ambos en una ronda.
- Si la solicitud no cabe, dilo en el plan, termina bien una parte coherente de ella y termina con la lista exacta de lo que queda, para que el usuario solo tenga que decir "continúa". Nunca te detengas en medio de una parte.
Reglas que previenen la mayoría de los fallos
- No elimines o sobrescribas lo que no creaste a menos que el usuario lo haya pedido. No
"seleccionar todo, eliminar" para obtener una escena limpia; no
read_factory_settings. Si el cubo por defecto está en medio, pregunta o trabaja a su lado. - Pon tu trabajo en su propia colección, con el nombre del modelo, y da a cada objeto
y malla un nombre real (
Silla_Pata_DT, noCubo.004). Pasos posteriores encuentran objetos por nombre. - Escala real. 1 unidad = 1 metro. Una puerta mide 2.0 m de alto, una taza 0.10 m. Verifica
obj.dimensionsdespués de construir en lugar de confiar en los números que escribiste. - Transformaciones: establece
location,rotation_euler(radianes: usamath.radians) yscaledirectamente; aplica la escala antes de biselados o exportaciones (consulta la referencia). - Prefiere datos sobre operadores.
bpy.dataybmeshfuncionan independientemente de lo que esté seleccionado;bpy.opsdepende del contexto y falla de formas difíciles de leer. Cuando debas usar un operador, selecciona y activa el objeto explícitamente primero. - Haz que los scripts sean re-ejecutables. Busca un objeto por nombre y reutilízalo o reemplázalo,
en lugar de crear
Pata.001,Pata.002en cada reintento. - Envía de vuelta lo que necesites. Termina una llamada con un
printcorto de nombres, dimensiones o conteos (en BlenderLab, o un diccionario asignado aresult) para que el siguiente paso comience a partir de hechos. - ¿No estás seguro de un nombre de API? La API de Python de Blender cambia entre versiones. Verifícalo con
la herramienta API de tu conexión antes de adivinar, y lee la versión de
bpy.app.version_stringuna vez al principio.
Los patrones para primitivas, bmesh, modificadores, suavizado, curvas, arreglos y herencia están en referencias/patrones-bpy.md. Ábrelo antes de escribir el primer script de una tarea.
Cómo elegir construir una forma
| Forma | Enfoque |
|---|---|
| Cajas, cilindros, muebles, arquitectura, accesorios | Primitivas, escaladas y posicionadas; modificador Bisel para bordes |
| Cualquier cosa repetida (valla, escaleras, dientes de un engranaje) | Una parte + modificador Matriz, o un bucle que enlace la misma malla |
| Objetos redondos o torneados (jarrón, vidrio, botella, rueda) | Un perfil + modificador Tornillo, o una curva con bisel |
| Tuberías, cables, mangos, rieles | Una curva con bevel_depth |
| Cosas simétricas (vehículo, bloqueo de personaje) | Mitad del modelo + modificador Espejo |
| Texto, logotipos, monedas, placas | Objeto de texto o curva, extruido, luego convertido a malla |
| Formas suaves y orgánicas | Bloqueo a partir de primitivas + Subdivisión de Superficie; di honestamente que el detalle esculpido es limitado de esta manera |
Mantén los modificadores sin aplicar mientras iteras (permanecen editables) y dilo en el informe; aplícalos solo para exportación o cuando un paso posterior necesite la geometría real.
Trabajar a partir de una imagen de referencia
- Describe lo que ves primero: las partes, sus proporciones relativas entre sí, y el conteo de cualquier cosa repetida. Construye a partir de esa lista.
- La imagen decide, no los números inventados para ella. Lee las proporciones como razones (ancho a alto a fondo; cuánto de la altura total es el techo; qué ancho tiene una puerta contra su pared), elige una medida para anclar la escala (la del usuario, o un tamaño sensato del mundo real) y deriva todos los demás tamaños de las razones. Si el texto de la solicitud da tamaños que contradicen la imagen, sigue la imagen y dilo.
- Cuando te pidan escribir un prompt o un resumen a partir de una referencia, describe formas, proporciones, colores y conteos. No inventes tamaños en metros: quien construya a partir del resumen los seguirá en lugar de la imagen.
- Construye lo que muestra la referencia. Sin base, accesorios o decoración que no tenga, a menos que el usuario lo haya pedido.
- Después del bloqueo, y de nuevo antes de reportar, coloca la vista en el ángulo de la referencia, captúrala y compárala parte por parte: silueta, proporciones, dónde se sienta cada parte, colores. Arregla lo que difiera; lo que no puedas arreglar, enuméralo en el informe. Espera dos o tres rondas de corrección; eso es normal, no un fallo.
- El encuadre, la luz y los colores se miden, no se juzgan a ojo: consulta "La verificación final" a continuación. Compara tu render con el archivo de referencia cuando le des el nombre del archivo.
- Una referencia adjunta a un mensaje anterior puede que ya no sea visible para ti. Si no puedes verla, dilo y pídela de nuevo en lugar de trabajar desde la memoria.
La verificación final (siempre tu última llamada)
No ves tus renders: las herramientas de render escriben un archivo y te dan su ruta, y una
captura de la ventana gráfica no muestra la luz de la escena. Un modelo que reporta desde lo que
pretendió, no desde lo que resultó, entrega imágenes blancas o negras sin saberlo. Así que la
última llamada execute_blender_code de cada tarea que construye o cambia un modelo es
final_check, y su salida impresa va a tu informe tal como está.
Hace un pequeño render desde la cámara de la escena, lo divide en fondo (los colores de las
cuatro esquinas) y modelo (el resto), e imprime: cuánto del encuadre llena el modelo,
su brillo, saturación y colores principales, los objetos sin material, los objetos
cuya geometría está rota (caras unidas a los vértices incorrectos), y una
línea PROBLEM: por cada cosa que está mal. Con el nombre del archivo de la referencia
mide esa imagen de la misma manera y compara el encuadre, el brillo, el color y qué tan sólida
está la silueta.
import bpy, os, re
import numpy as np
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
BACKDROP_WORDS = ("ground", "backdrop", "floor", "sky", "shadow")
def subject_stats(path, size=128):
"""Una imagen dividida en fondo (los colores de sus cuatro esquinas) y sujeto (el resto):
cuánto ocupa el sujeto de la imagen, su caja, su brillo, saturación y colores."""
img = bpy.data.images.load(path, check_existing=False)
w, h = img.size
sw, sh = (size, max(8, round(size * h / w))) if w >= h else (max(8, round(size * w / h)), size)
img.scale(sw, sh)
px = np.empty(len(img.pixels), dtype=np.float32)
img.pixels.foreach_get(px)
bpy.data.images.remove(img)
rgb = px.reshape(sh, sw, 4)[:, :, :3] # la fila 0 es la parte inferior de la imagen
cy, cx = max(2, sh // 8), max(2, sw // 8)
corners = np.concatenate([rgb[:cy, :cx].reshape(-1, 3), rgb[:cy, -cx:].reshape(-1, 3),
rgb[-cy:, :cx].reshape(-1, 3), rgb[-cy:, -cx:].reshape(-1, 3)])
# Fondo = cercano a uno de los colores de las esquinas (un degradado o una sombra suave aún cuenta).
q = np.unique(np.round(corners * 8).astype(int), axis=0)
flat = rgb.reshape(-1, 3)
dist = np.min(np.abs(flat[:, None, :] * 8 - q[None, :, :]).max(axis=2), axis=1)
subject = (dist > 1.0).reshape(sh, sw)
share = float(subject.mean())
whole = flat @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
out = {"size": (w, h), "image_brightness": round(float(whole.mean()), 3), "subject_pct": round(100 * share, 1)}
if share < 0.005:
return out
ys, xs = np.nonzero(subject)
out["box"] = {"width": round(float(xs.max() - xs.min() + 1) / sw, 2), "height": round(float(ys.max() - ys.min() + 1) / sh, 2),
"center": (round(float(xs.max() + xs.min() + 1) / 2 / sw, 2), round(float(ys.max() + ys.min() + 1) / 2 / sh, 2))}
sub = rgb[subject]
lum = sub @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
mx, mn = sub.max(axis=1), sub.min(axis=1)
out["brightness"] = round(float(lum.mean()), 3)
out["saturation"] = round(float(np.where(mx > 1e-4, (mx - mn) / np.maximum(mx, 1e-4), 0.0).mean()), 3)
out["burnt_pct"] = round(100 * float((lum > 0.97).mean()), 1)
keys = (np.clip((sub * 5.999).astype(int), 0, 5) * np.array([36, 6, 1])).sum(axis=1)
counts = np.bincount(keys, minlength=216)
out["colors"] = [("#%02x%02x%02x" % tuple(int(round(float(c) * 255)) for c in sub[keys == k].mean(axis=0)),
int(round(100 * counts[k] / len(keys)))) for k in counts.argsort()[::-1][:5] if counts[k]]
return out
# Dónde una instalación de HydraOps guarda sus datos (storage/uploads cuelga de él): la carpeta
# de datos que se le indicó (HYDRA_DATA_DIR), la carpeta del instalador por sistema, y un clon
# del repositorio, que mantiene sus datos dentro de sí mismo cuando se ejecuta desde el código fuente.
UPLOAD_ROOTS = [os.environ.get("HYDRA_DATA_DIR", ""),
os.path.join(os.environ.get("APPDATA", ""), "HydraOps", "data"),
os.path.expanduser("~/Library/Application Support/HydraOps/data"),
os.path.expanduser("~/.config/HydraOps/data"),
os.path.expanduser("~/HydraOps")]
UPLOAD_FOLDERS = [os.path.join(r, "storage", "uploads") for r in UPLOAD_ROOTS if r]
def find_reference(file_name):
"""La ruta de una imagen adjunta en ESTA conversación, nombrada como la conversación la muestra:
"storage/uploads/1791118524833-casa.png", la simple "1791118524833-casa.png", o una ruta completa.
None cuando no está allí."""
raw = str(file_name or "").replace("\\", "/").strip()
name = os.path.basename(raw)
if not name or os.path.splitext(name)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"):
return None
if os.path.isabs(raw) and os.path.isfile(raw):
return os.path.normpath(raw)
for folder in UPLOAD_FOLDERS:
path = os.path.join(folder, name)
if os.path.isfile(path):
return path
# El nombre almacenado comienza con una marca de tiempo que es fácil de copiar con un dígito incorrecto:
# recurre a la subida más reciente con el mismo nombre original después de la marca de tiempo.
m = re.match(r"^\d{10,}-(.+)$", name)
if m:
pattern = re.compile(r"^\d{10,}-" + re.escape(m.group(1)) + "$")
newest = None
for folder in UPLOAD_FOLDERS:
try:
entries = os.listdir(folder)
except OSError:
continue
for entry in entries:
...
[truncado para la traducción]