← Volver al catálogo
WebReferenciaAvanzadoGratis

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.

Descargar SKILL.md

Descarga abierta · sin registro · para Blender

// resultado_de_ejemplo

""" 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

Blender
Categoría
Web
Tipo
Referencia
Nivel
Avanzado
Licencia
Apache-2.0
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// 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 €.