Desarrollo de Add-ons para Blender
Guia tecnica para crear add-ons personalizados en Blender con Python, cubriendo operadores, paneles UI, propiedades y empaquetado para distribucion. Ideal para extender Blender con flujos de trabajo propios o herramientas a medida para clientes de diseno 3D.
Descarga abierta · sin registro · para Blender
""" PixelForge Export Suite — Add-on para Blender Cliente: PixelForge Studio (Madrid) Skill: desarrollo-addons-blender | CULTIVA IA
Archivo standalone (todo en un fichero) listo para instalar via: Edit > Preferences > Add-ons > Install...
Para la version multi-archivo, ver la seccion "ESTRUCTURA MULTI-ARCHIVO" al final de este fichero. """
=============================================================================
METADATA DEL ADD-ON
=============================================================================
bl_info = { "name": "PixelForge Export Suite", "author": "CULTIVA IA para PixelForge Studio", "version": (1, 2, 0), "blender": (3, 6, 0), "location": "View3D > Sidebar > PixelForge", "description": ( "Exportacion rapida a GLB/FBX y renombrado por lotes para flujos " "de trabajo de visualizacion arquitectonica y producto 3D." ), "doc_url": "https://cultivaia.com", "category": "Import-Export", }
import bpy import os import re from bpy.types import ( Operator, Panel, PropertyGroup, AddonPreferences, ) from bpy.props import ( StringProperty, BoolProperty, FloatProperty, EnumProperty, IntProperty, PointerProperty, )
=============================================================================
1. PROPIEDADES / SETTINGS
=============================================================================
class PF_Settings(PropertyGroup): """Propiedades persistentes del add-on, almacenadas en la escena."""
# -- Exportacion GLB --
export_path: StringProperty(
name="Ruta de exportacion",
description="Directorio donde se guardan los archivos exportados",
default="//exports/",
subtype='DIR_PATH',
)
export_scale: FloatProperty(
name="Escala",
description="Factor de escala aplicado en la exportacion",
default=1.0,
min=0.001,
max=1000.0,
step=10,
precision=3,
)
apply_modifiers: BoolProperty(
name="Aplicar modificadores",
description="Aplica todos los modificadores antes de exportar",
default=True,
)
include_materials: BoolProperty(
name="Incluir materiales",
description="Exporta los materiales y texturas embebidos en el GLB",
default=True,
)
export_format: EnumProperty(
name="Formato",
description="Formato de exportacion",
items=[
('GLB', "GLB (binario)", "Formato GLB compacto, ideal para web y realidad aumentada"),
('GLTF', "glTF (JSON)", "Formato glTF legible, separa texturas en ficheros externos"),
('FBX', "FBX", "Formato FBX para pipelines de animacion y motores como Unreal"),
],
default='GLB',
)
one_file_per_object: BoolProperty(
name="Un fichero por objeto",
description="Exporta cada objeto seleccionado en su propio fichero",
default=False,
)
# -- Renombrado por lotes --
rename_prefix: StringProperty(
name="Prefijo",
description="Prefijo que se antepone a todos los objetos",
default="PF_2024_",
)
rename_separator: EnumProperty(
name="Separador",
items=[
('_', "Guion bajo (_)", ""),
('-', "Guion medio (-)", ""),
('.', "Punto (.)", ""),
('', "Sin separador", ""),
],
default='_',
)
rename_start: IntProperty(
name="Numero inicial",
default=1,
min=0,
max=9999,
)
rename_padding: IntProperty(
name="Digitos",
description="Numero de digitos con relleno de ceros (ej. 3 = 001)",
default=3,
min=1,
max=6,
)
rename_strip_existing: BoolProperty(
name="Limpiar nombre base",
description="Elimina numeros y prefijos existentes antes de renombrar",
default=False,
)
=============================================================================
2. OPERADORES
=============================================================================
class PF_OT_export_glb(Operator): """Exporta los objetos seleccionados a GLB/glTF/FBX con las opciones del panel.""" bl_idname = "pixelforge.export_glb" bl_label = "Exportar seleccion" bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (
context.selected_objects
and context.mode == 'OBJECT'
)
def execute(self, context):
settings = context.scene.pf_settings
export_dir = bpy.path.abspath(settings.export_path)
# Crear directorio si no existe
os.makedirs(export_dir, exist_ok=True)
selected = list(context.selected_objects)
fmt = settings.export_format.lower()
ext_map = {'glb': 'glb', 'gltf': 'gltf', 'fbx': 'fbx'}
ext = ext_map.get(fmt, 'glb')
scene_name = bpy.path.clean_name(context.scene.name)
exported = 0
errors = []
if settings.one_file_per_object:
# --- Un fichero por objeto ---
active_orig = context.active_object
for obj in selected:
# Desseleccionar todo y seleccionar solo este objeto
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
filepath = os.path.join(
export_dir,
f"{scene_name}_{bpy.path.clean_name(obj.name)}.{ext}"
)
ok = self._do_export(context, settings, filepath, fmt)
if ok:
exported += 1
else:
errors.append(obj.name)
# Restaurar seleccion original
for obj in selected:
obj.select_set(True)
context.view_layer.objects.active = active_orig
else:
# --- Todo en un solo fichero ---
filepath = os.path.join(export_dir, f"{scene_name}_export.{ext}")
ok = self._do_export(context, settings, filepath, fmt)
if ok:
exported = len(selected)
else:
errors = [o.name for o in selected]
if errors:
self.report({'WARNING'}, f"Exportacion con errores en: {', '.join(errors)}")
else:
self.report(
{'INFO'},
f"Exportados {exported} objeto(s) a {export_dir} [{fmt.upper()}]"
)
return {'FINISHED'}
# ------------------------------------------------------------------
def _do_export(self, context, settings, filepath, fmt):
"""Llama al exportador de Blender segun el formato elegido."""
try:
if fmt in ('glb', 'gltf'):
bpy.ops.export_scene.gltf(
filepath=filepath,
export_format=settings.export_format,
use_selection=True,
export_apply=settings.apply_modifiers,
export_materials='EXPORT' if settings.include_materials else 'NONE',
export_scale=settings.export_scale,
)
elif fmt == 'fbx':
bpy.ops.export_scene.fbx(
filepath=filepath,
use_selection=True,
apply_scale_options='FBX_SCALE_ALL',
global_scale=settings.export_scale,
use_mesh_modifiers=settings.apply_modifiers,
path_mode='COPY',
embed_textures=settings.include_materials,
)
return True
except Exception as exc:
print(f"[PixelForge] Error exportando {filepath}: {exc}")
return False
----------------------------------------------------------------------
class PF_OT_batch_rename(Operator): """Renombra todos los objetos seleccionados con prefijo + numeracion correlativa.""" bl_idname = "pixelforge.batch_rename" bl_label = "Renombrar seleccion" bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bool(context.selected_objects) and context.mode == 'OBJECT'
def execute(self, context):
settings = context.scene.pf_settings
objects = sorted(context.selected_objects, key=lambda o: o.name)
prefix = settings.rename_prefix
sep = settings.rename_separator
padding = settings.rename_padding
n = settings.rename_start
renamed = []
for obj in objects:
base = obj.name
if settings.rename_strip_existing:
# Elimina secuencias numericas y prefijos existentes al inicio
base = re.sub(r'^[A-Z0-9_\-\.]+\d+[_\-\.]?', '', base, flags=re.IGNORECASE)
base = base.strip('_-. ') or "Object"
new_name = f"{prefix}{str(n).zfill(padding)}{sep}{base}"
renamed.append(f" {obj.name!r} -> {new_name!r}")
obj.name = new_name
n += 1
summary = "\n".join(renamed)
print(f"[PixelForge] Renombrado:\n{summary}")
self.report({'INFO'}, f"Renombrados {len(objects)} objeto(s) con prefijo '{prefix}'")
return {'FINISHED'}
----------------------------------------------------------------------
class PF_OT_open_export_folder(Operator): """Abre el directorio de exportacion en el explorador de archivos del sistema.""" bl_idname = "pixelforge.open_export_folder" bl_label = "Abrir carpeta"
def execute(self, context):
path = bpy.path.abspath(context.scene.pf_settings.export_path)
os.makedirs(path, exist_ok=True)
import subprocess, sys
if sys.platform == 'win32':
os.startfile(path)
elif sys.platform == 'darwin':
subprocess.Popen(['open', path])
else:
subprocess.Popen(['xdg-open', path])
return {'FINISHED'}
=============================================================================
3. PANEL UI
=============================================================================
class VIEW3D_PT_pixelforge_export(Panel): """Panel principal del add-on PixelForge en el N-panel del viewport 3D.""" bl_label = "Exportacion Rapida" bl_idname = "VIEW3D_PT_pixelforge_export" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "PixelForge"
def draw(self, context):
layout = self.layout
settings = context.scene.pf_settings
selected = context.selected_objects
# -- Cabecera con conteo --
header = layout.box()
row = header.row()
row.label(text=f"Seleccionados: {len(selected)}", icon='OBJECT_DATA')
if not selected:
row.label(text="(ninguno)", icon='ERROR')
layout.separator()
# -- Opciones de exportacion --
box = layout.box()
box.label(text="Configuracion", icon='SETTINGS')
box.prop(settings, "export_format")
box.prop(settings, "export_path")
col = box.column(align=True)
col.prop(settings, "export_scale")
col.prop(settings, "apply_modifiers")
col.prop(settings, "include_materials")
col.prop(settings, "one_file_per_object")
layout.separator()
# -- Boton de exportacion --
row = layout.row(align=True)
row.scale_y = 1.5
export_op = row.operator(
"pixelforge.export_glb",
text=f"Exportar {len(selected)} objeto(s)",
icon='EXPORT',
)
row.operator("pixelforge.open_export_folder", text="", icon='FILE_FOLDER')
# Atajo de teclado como hint
layout.label(text="Atajo: Ctrl+Shift+E", icon='KEYINGSET')
class VIEW3D_PT_pixelforge_rename(Panel): """Sub-panel de renombrado por lotes.""" bl_label = "Renombrado por Lotes" bl_idname = "VIEW3D_PT_pixelforge_rename" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "PixelForge" bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
settings = context.scene.pf_settings
selected = context.selected_objects
box = layout.box()
box.label(text="Patron de nombre", icon='FONT_DATA')
box.prop(settings, "rename_prefix")
box.prop(settings, "rename_separator")
row = box.row(align=True)
row.prop(settings, "rename_start")
row.prop(settings, "rename_padding")
box.prop(settings, "rename_strip_existing")
# Vista previa del patron
if selected:
first = sorted(selected, key=lambda o: o.name)[0]
base = first.name
if settings.rename_strip_existing:
base = re.sub(r'^[A-Z0-9_\-\.]+\d+[_\-\.]?', '', base, flags=re.IGNORECASE).strip('_-. ') or "Object"
preview = (
f"{settings.rename_prefix}"
f"{str(settings.rename_start).zfill(settings.rename_padding)}"
f"{settings.rename_separator}{base}"
)
layout.label(text=f"Vista previa: {preview}", icon='INFO')
layout.separator()
row = layout.row()
row.scale_y = 1.4
row.operator(
"pixelforge.batch_rename",
text=f"Renombrar {len(selected)} objeto(s)",
icon='SORTALPHA',
)
layout.label(text="Atajo: Ctrl+Shift+R", icon='KEYINGSET')
=============================================================================
4. PREFERENCIAS DEL ADD-ON
=============================================================================
class PF_AddonPreferences(AddonPreferences): """Preferencias globales del add-on (Edit > Preferences > Add-ons).""" bl_idname = name
studio_name: StringProperty(
name="Nombre del estudio",
default="PixelForge Studio",
)
default_export_path: StringProperty(
name="Ruta de exportacion por defecto",
subtype='DIR_PATH',
default="//exports/",
)
show_hints: BoolProperty(
name="Mostrar atajos de teclado en el panel",
default=True,
)
def draw(self, context):
layout = self.layout
layout.prop(self, "studio_name")
layout.prop(self, "default_export_path")
layout.prop(self, "show_hints")
=============================================================================
5. KEYMAPS
=============================================================================
addon_keymaps = []
def register_keymaps(): wm = bpy.context.window_manager kc = wm.keyconfigs.addon if not kc: return
km = kc.keymaps.new(name='Object Mode', space_type='EMPTY')
# Ctrl+Shift+E — Exportar
kmi_export = km.keymap_items.new(
"pixelforge.export_glb",
type='E', value='PRESS',
ctrl=True, shift=True,
)
addon_keymaps.append((km, kmi_export))
# Ctrl+Shift+R — Renombrar
kmi_rename = km.keymap_items.new(
"pixelforge.batch_rename",
type='R', value='PRESS',
ctrl=True, shift=True,
)
addon_keymaps.append((km, kmi_rename))
def unregister_keymaps(): for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear()
=============================================================================
6. REGISTRO / DESREGISTRO
=============================================================================
Orden: PropertyGroups primero, luego Operators, luego Panels, luego Prefs
CLASSES = ( PF_Settings, PF_OT_export_glb, PF_OT_batch_rename, PF_OT_open_export_folder, VIEW3D_PT_pixelforge_export, VIEW3D_PT_pixelforge_rename, PF_AddonPreferences, )
def register(): for cls in CLASSES: bpy.utils.register_class(cls)
# Asociar el PropertyGroup a la escena activa
bpy.types.Scene.pf_settings = PointerProperty(type=PF_Settings)
# Registrar atajos de teclado
register_keymaps()
print("[PixelForge Export Suite] Add-on activado.")
def unregister(): unregister_keymaps()
# Eliminar propiedad de la escena
del bpy.types.Scene.pf_settings
# Desregistrar en orden inverso
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)
print("[PixelForge Export Suite] Add-on desactivado.")
if name == "main": register()
=============================================================================
ESTRUCTURA MULTI-ARCHIVO (para distribucion como .zip)
=============================================================================
pixelforge_export_suite/
├── init.py ← bl_info + imports + register/unregister
├── properties.py ← PF_Settings (PropertyGroup)
├── operators.py ← PF_OT_export_glb, PF_OT_batch_rename, PF_OT_open_export_folder
├── panels.py ← VIEW3D_PT_pixelforge_export, VIEW3D_PT_pixelforge_rename
├── keymaps.py ← register_keymaps, unregister_keymaps, addon_keymaps
├── prefs.py ← PF_AddonPreferences
└── utils.py ← helpers: clean_name, make_export_path, strip_existing_prefix
init.py (version multi-archivo):
---
bl_info = {
"name": "PixelForge Export Suite",
"author": "CULTIVA IA para PixelForge Studio",
"version": (1, 2, 0),
"blender": (3, 6, 0),
"location": "View3D > Sidebar > PixelForge",
"description": "Exportacion rapida GLB/FBX y renombrado por lotes.",
"category": "Import-Export",
}
import bpy
from . import properties, operators, panels, keymaps, prefs
MODULES = (properties, operators, panels, keymaps, prefs)
def register():
for mod in MODULES:
mod.register()
bpy.types.Scene.pf_settings = bpy.props.PointerProperty(
type=properties.PF_Settings
)
keymaps.register_keymaps()
def unregister():
keymaps.unregister_keymaps()
del bpy.types.Scene.pf_settings
for mod in reversed(MODULES):
mod.unregister()
Empaquetar: zip -r pixelforge_export_suite.zip pixelforge_export_suite/
Instalar: Edit > Preferences > Add-ons > Install > seleccionar el .zip
=============================================================================
// qué_hace
Proporciona patrones y ejemplos de codigo para construir add-ons de Blender con operadores, paneles UI, propiedades personalizadas, atajos de teclado y estructura de distribucion.
// cómo_lo_hace
Usa la API de Python de Blender (bpy) con clases registrables, convenciones de nomenclatura estrictas y ciclos register/unregister para integrar herramientas directamente en la interfaz de Blender.
// ejemplo_de_uso
Al extender Blender con herramientas propias para flujos de trabajo 3D repetitivos. Ej.: creas un add-on que añade un panel en el viewport para renombrar objetos por lotes y lo distribuyes como .zip instalable desde las preferencias.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.