Modelado low-poly en Blender
Low poly es un estilo, no solo un bajo recuento de triángulos: formas grandes y legibles, facetas planas visibles, unos pocos colores sólidos y proporciones ligeramente exageradas más allá de la realidad. Un modelo realista con menos polígonos no es low poly; es un modelo realista basto.
El bucle de trabajo general (mirar la escena, planificar en partes, una parte por llamada, captura de pantalla después de cada paso, nunca borrar lo que no creaste) está en la habilidad blender-modeling: síguelo. Esta habilidad indica qué es diferente para low poly.
Tu conexión Blender
Trabajas a través de BlenderLab, el servidor oficial de Blender Lab; sus herramientas son blenderlab_…:
- Lo que 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. - Ejecuta Python:
execute_blender_code. Nada persiste entre llamadas (lleva tus ayudas en cada script); lo que imprime el script regresa, y un diccionario asignado aresultregresa como datos. Su guardia rechaza algunos operadores (salir de Blender, restablecimientos de fábrica). - Ve el viewport 3D:
get_screenshot_of_area_as_imageconarea_ui_type: "VIEW_3D"(la ventana completa:get_screenshot_of_window_as_image); apúntalo a un objeto:jump_to_view3d_object_by_name. - Verifica 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 dilo: 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 si no pudiste hacerlo.
El estilo, como reglas
- Sombreado plano en todas partes. Cada cara es un color plano; sin sombreado suave, sin auto smooth. Esto es lo que hace que los facetados se vean.
- Pocos segmentos, a propósito. Cilindros de 6-8 lados, conos de 5-6, esferas como icósferas con 1-2 subdivisiones. Un cilindro de 32 lados arruina la apariencia más que cualquier otra cosa.
- Sin bisel, sin Subdivision Surface. Redondean los facetados. Para un borde más suave, usa un chaflán de un solo segmento como máximo.
- Color sólido, sin texturas. Un material por color, Principled BSDF con Base Color, Roughness 0.8-1.0, Metallic 0, sin brillo especular. Reutiliza materiales: una escena completa normalmente necesita 6-12.
- Una paleta deliberada. Elígela antes de modelar: 1-2 colores dominantes, 2-3 de apoyo, 1 acento. Tonos medios ligeramente desaturados, equilibrados entre cálidos y fríos, y dos tonos de cada color principal (claro y oscuro) para variedad entre partes vecinas.
- Proporciones robustas y exageradas. Paredes, vigas y troncos más gruesos que en la vida real; techos, puertas y ventanas sobredimensionados; formas cónicas (más anchas en la base, o en la parte superior para un aspecto caricaturesco). Las partes delgadas desaparecen en este nivel de detalle.
- Imperfección a mano, en pequeñas dosis. Nada perfectamente alineado: inclina una chimenea 2-4°, varía las tejas en un pequeño porcentaje de su tamaño y en 2-3° de inclinación, mueve los vértices de rocas y terreno al azar. La estructura aún debe leerse: las filas de tejas siguen siendo filas, ninguna pieza girada de lado o levantada de la superficie. Con una semilla aleatoria fija, para que una re-ejecución dé el mismo resultado.
- Detalle mediante piezas pequeñas separadas, no mediante subdivisión. Una ventana es un marco de cuatro cajas más un panel; las tejas del techo son filas de pequeñas losas; las piedras son algunos bloques irregulares incrustados en la pared. Cada pieza es simple; la disposición da la riqueza.
- Las piezas pueden intersecarse. Las partes low-poly se empujan unas dentro de otras en lugar de soldarse: una viga se hunde en una pared, una roca en el suelo. No gastes esfuerzo en booleanos o uniones limpias.
Flujo de trabajo
- Lee la referencia, si la hay: enumera las partes, cuenta lo que se repite (filas de tejas, número de vigas, ventanas por pared), anota las proporciones entre partes como relaciones (altura del techo frente a altura de la pared, ancho frente a altura), y nombra los colores de la paleta. Construye a partir de esa lista. La imagen decide: fija un tamaño y deriva el resto de las relaciones, no inventes tamaños en metros que la imagen contradiga, y no añadas lo que no muestra (una base, accesorios) a menos que se te pida.
- Bloqueo primero las masas grandes con cajas y cuñas en las proporciones correctas. Toma una captura de pantalla y compara la silueta: si la silueta está mal, los detalles no la salvarán.
- Paleta: crea los materiales una vez, nombrados por lo que colorean (
LP_Wall_Light,LP_Roof_Red,LP_Wood_Dark). - Estructura, luego partes secundarias (vigas, marcos, chimenea, escalones), luego pequeños detalles (tejas, piedras, tablones, grietas), luego alrededores (losa de suelo, algunas rocas, matas de hierba).
- Rugosidad: los desplazamientos e inclinaciones aleatorios de la regla 7.
- Luz y encuadre (abajo), luego coloca la vista en el ángulo de la referencia, captúrala y compara parte por parte: silueta, proporciones, dónde se encuentra cada parte, colores. Corrige.
- Termina con
final_check, la última llamada de la tarea: la función está abajo, en "La comprobación final". No ves tus renders; mide el render por ti (el modelo aparte del fondo, contra la imagen de referencia cuando tienes su nombre de archivo) e imprime una líneaPROBLEM:para lo que está mal. Corrige, ejecútalo de nuevo (como máximo dos rondas) y pon su salida en el informe. Las mediciones que hagas tú mismo no lo reemplazan: una tarea que construye o modifica un modelo no está terminada hasta que esta función se haya ejecutado.
Espera dos o tres rondas de corrección contra la referencia. Di lo que aún difiere.
Tienes un número limitado de rondas de herramientas por tarea (consulta "Tu presupuesto de pasos" en blender-modeling): construye dos o tres partes relacionadas por llamada, captura en los hitos (bloqueo, detalles, final) y pon las partes más visibles primero.
El código para cada receta está en references/low-poly-recipes.md: paleta, sombreado plano, cajas cónicas, techos a dos aguas, filas de tejas, ventanas y puertas, entramados de madera, rocas, árboles, terreno, dispersión y la configuración de iluminación. Ábrelo antes del primer script.
Muchos bloques en un objeto
Hiladas de piedras, almenas, adoquinado, tablones y ladrillos son muchas cajas pequeñas que pertenecen a un solo objeto. Constrúyelos con lp_blocks: termina cada caja en su propio bmesh y solo entonces la añade al objeto. No unas cajas agregando listas de vértices y caras a mano, y no muevas "los últimos N vértices" después de una operación que cambie su número (un chaflán lo hace): las caras terminan en los vértices equivocados y el modelo sale como picos estirados. final_check nombra un objeto roto así; reconstrúyelo, no lo reportes.
import bpy, bmesh
from mathutils import Matrix, Vector
def lp_blocks(name, col, mat, blocks, chamfer=0.03):
"""Muchas cajas en UN objeto con sombreado plano. blocks es una lista de (centro, tamaño) o
(centro, tamaño, rotación_z): centro y tamaño son (x, y, z) en metros, la rotación en radianes.
Cada caja se termina en su propio bmesh y solo entonces se añade al objeto."""
whole = bmesh.new()
for center, size, *rot in blocks:
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bmesh.ops.scale(bm, vec=Vector(size), verts=bm.verts)
if chamfer > 0:
bmesh.ops.bevel(bm, geom=bm.edges[:], offset=min(chamfer, min(size) * 0.25), segments=1, affect='EDGES')
bmesh.ops.transform(bm, matrix=Matrix.Translation(Vector(center)) @ Matrix.Rotation(rot[0] if rot else 0.0, 4, 'Z'), verts=bm.verts)
piece = bpy.data.meshes.new("_piece")
bm.to_mesh(piece)
bm.free()
whole.from_mesh(piece) # añade la caja terminada a la malla del objeto
bpy.data.meshes.remove(piece)
mesh = bpy.data.meshes.new(name)
whole.to_mesh(mesh)
whole.free()
for poly in mesh.polygons:
poly.use_smooth = False
obj = bpy.data.objects.get(name)
if obj is None:
obj = bpy.data.objects.new(name, mesh)
col.objects.link(obj)
else:
obj.data = mesh
obj.data.materials.append(mat)
return obj
# Una hilada de piedras de muro, alternando dos tamaños, en un solo objeto.
col = bpy.data.collections["LP_Model"]
stone = bpy.data.materials["LP_Stone_Light"]
lp_blocks("Wall_Course_01", col, stone,
[((x * 0.42 - 1.26, -1.0, 0.35), (0.40 if x % 2 else 0.36, 0.20, 0.30)) for x in range(7)])
Formas y cómo obtenerlas
| Quieres | Constrúyelo como |
|---|---|
| Paredes, vigas, tablones, escalones | Cajas, escaladas; afina la parte superior o inferior para dar carácter |
| Bloques de piedra, almenas, adoquines, ladrillos | lp_blocks (arriba): muchas cajas con chaflán en un objeto |
| Techo a dos aguas | Un prisma (perfil triangular extruido), sobresaliendo de las paredes por todos lados |
| Tejas | Filas de losas delgadas en la pendiente del techo, cada fila superponiendo la de abajo, con pequeño tamaño e inclinación aleatorios |
| Chimenea, torre, pilar | Caja cónica; losa de tapa ligeramente más ancha |
| Árbol | Tronco: cilindro cónico de 5-6 lados. Follaje: 2-3 conos apilados (pino) o 1-3 icósferas desplazadas (frondoso) |
| Roca | Icósfera con 1 subdivisión, vértices movidos aleatoriamente, aplanada en Z, hundida en el suelo |
| Terreno, isla | Una rejilla con pocos cortes, vértices elevados por ruido, luego sombreado plano; o una losa gruesa irregular para una base de diorama |
| Agua | Un plano o losa plana, azul, ligeramente brillante (Roughness 0.2) |
| Hierba, arbustos | Conos pequeños o icósferas aplanadas en grupos |
| Cosas redondas (barril, rueda, pozo) | Cilindros de 6-8 lados |
Presupuestos
Recuentos aproximados de triángulos, para juzgar si algo está sobreconstruido:
| Activo | Triángulos |
|---|---|
| Accesorio pequeño (roca, caja, arbusto) | 20-150 |
| Árbol | 60-300 |
| Casa o edificio | 500-3,000 |
| Personaje | 300-1,500 |
| Diorama completo | 3,000-15,000 |
Imprime el recuento cuando termines. Muy por encima del presupuesto generalmente significa demasiados segmentos o un modificador que subdivide.
Luz y presentación
El estilo depende de la luz: los facetados solo se leen cuando las caras vecinas obtienen diferente brillo.
- Un sol en ángulo (aproximadamente 45° arriba, 30-45° al lado), intensidad 3-4, con sombras suaves; más un mundo de un color de cielo claro con intensidad 0.6-1.0 para que las sombras no sean negras. Sin configuración de estudio de tres puntos. No subas la luz para obtener un fondo blanco, y no tiñas el mundo para colorearlo: el propio material del fondo da el color.
- Un sol ligeramente cálido contra un mundo ligeramente frío da el aspecto clásico.
- Configura la transformación de vista a
Standardpara que los colores de la paleta se mantengan como se eligieron (la transformación por defecto los vuelve grises). - Cámara: vista de tres cuartos desde arriba (30-35° hacia abajo), lente larga (50-85 mm) u ortográfica para un diorama.
- Un fondo sólido en un color de la paleta, o una losa de suelo que termine en un borde limpio. Un plano de fondo debe ser mucho más grande que el modelo (20 veces o más), o el mundo del mismo color: ningún borde u horizonte puede mostrarse en el encuadre.
- EEVEE es suficiente y rápido. Las sombras suaves y la oclusión ambiental ayudan mucho.
Los materiales, el renderizado a archivo y la solución de problemas están en blender-materials-render.
Para un motor de juego
Los activos low-poly generalmente van a un motor. Mantén el sombreado plano al exportar (los facetados se almacenan como normales divididas, lo que aumenta el recuento de vértices pero no el de triángulos), fusiona las piezas de un activo en una sola malla con unos pocos espacios de material, y coloca el origen en el centro inferior. Sigue blender-game-export para el resto.
La comprobación final (siempre tu última llamada)
Las herramientas de renderizado escriben un archivo y te dan su ruta: nunca ves la imagen. Así que la última llamada execute_blender_code de la tarea es final_check, copiada tal cual, y lo que imprime va al informe tal cual. Hace un pequeño render desde la cámara de la escena y nombra lo que está mal: objetos sin material, geometría rota, un modelo fuera del encuadre, un render quemado u oscuro, y, con el nombre del archivo de referencia, las diferencias en encuadre, brillo, color y qué tan sólida es 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 entre fondo (los colores de sus cuatro esquinas) y sujeto (el resto):
cuánto de la imagen ocupa el sujeto, 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
…
[truncado para la traducción]