Pourquoi un addon de palettes dans Blender ?
Je suis Jean-Marie Rudent, motion designer 3D. Récemment, j’ai eu besoin d’avoir des couleurs spécifiques et réutilisables pour mes shaders.

J’ai cherché des addons de palettes pour le shading editor, mais j’ai pas trouvé facilement. Je me suis dit: « Bon allez, je vais me le faire moi même alors ».
Il existe déjà des palettes
Blender a bien un système de palettes, mais il est caché dans des outils comme le Grease Pencil ou la Texture Paint, donc inaccessible directement quand on bosse dans le Shading Editor.

Pour un projets de motion design 3D, j’avais besoin d’un accès rapide à des palettes personnalisées, sans perdre du temps à chercher des couleurs, à utiliser le eye-dropper à chaque fois sur une image externe ou copier-coller des valeurs RGB à la main.
Solution : un addon de palettes dans le shader editor de Blender
Fonctionnalités principales :
- Créer une bibliothèque de palettes
- Pouvoir renommer les palettes
- Chaque palette peut contenir jusqu’à 10 couleurs
- Pouvoir renommer les couleurs
- Les couleurs sont directement accessibles via un color picker natif de Blender
- Accessible depuis le N-Panel du Shading Editor
Mon process de flemmard :
1. Première tentative avec Mercury
Pour faire un addon sur blender, il faut faire un peu de python. Mais je connais pas le python. Donc j’ai utilisé Mercury de Inception Labs, une IA spécialisée dev, pour qu’elle me génère la structure de mon addon. Première déception: Mercury me sortait uniquement des valeurs RGBA classiques, c’est-à-dire des champs numériques pour chaque canal (R, G, B, A), mais sans le bouton color picker de Blender, celui avec la roue chromatique.

2. Recherche de la bonne propriété
Pour corriger ça, j’ai posé la question suivante à ChatGPT :
« Dans Blender, comment on appelle ce bouton de sélection de couleur pour un addon Python ? S’il te plaît »
Et là, jackpot : ChatGPT m’a guidé vers la bonne solution : utiliser une FloatVectorProperty avec subtype='COLOR'. Ça m’a permis d’avoir le vrai bouton color picker Blender, propre et standard.


3. C’est bien mais je veux des palettes, pas simplement une liste de couleurs
Ce qu’il me fallait vraiment, c’était une liste de palettes, chaque palette contenant jusqu’à 10 couleurs (nombre arbitraire). J’ai donc demandé à Mercury de repenser la structure de l’addon.
4. Et voilà, l’addon fait exactement ce que je veux

Une liste de palettes, dans le N-Panel du shading editor, avec les noms des couleurs et des palettes qu’on peut éditer.
Ça marche, missa content.
Limites actuelles de l’addon
Ce qui manque
Actuellement, l’addon ne sauvegarde les palettes que dans le fichier .blend dans lequel elles ont été créées. On ne peut pas transférer facilement les palettes d’un projet Blender à un autre.
Une meilleure solution à explorer
La bonne approche serait probablement de s’appuyer sur les systèmes de palettes existants de Blender, notamment ceux intégrés au mode Texture Paint. Ça permettrait d’avoir une gestion cohérente avec le reste du logiciel et de rendre les palettes partageables entre projets.

Je sais pas encore si je pourrais faire en sorte de pouvoir éditer les noms des couleurs, ce qui est pas mal pour se rappeler de leurs utilités.
Mon objectif initial était de faire un outil rapide, pratique et minimaliste. Cette solution simple répond à mon besoin immédiat, mais j’envisage une évolution future pour aller plus loin, ça peut aller tellement vite avec Mercury.
Mercury Coder est assez dingue en terme de rapidité. Il fonctionne différemment des autres LLM classiques, il repose sur un système de diffusion de réponse totale, comme les générateurs d’images, contrairement aux autres qui font de la prédiction de manière plus linéaire, mot après mot.

Pourquoi mon addon est utile pour les créateurs 3D et motion designers ?
Prenons un exemple concret :
Tu crées un pack d’assets 3D pour un client. Il y a une charte graphique à respecter avec des couleurs bien définies. Avec cet addon, tu peux enregistrer cette palette une bonne fois pour toutes, et la rappeler instantanément depuis le shadng editor.
- Gain de temps
- Cohérence chromatique assurée
- Adapté aux workflows pros (motion design, packshot produit, charte de jeu vidéo, etc.)
- Moult K€ à la clé
Mon addon est dispo sur Gumroad
En prix libre. Si c’est pour juste tester, c’est gratuit. Si tu veux soutenir mon travail d’explorateur python, tu peux donner ce que tu veux (beaucoup d’argent je veux bien)
Le script complet
Pour les amateurs de ctrl+c ctrl+v :
Script :
bl_info = {
"name": "Global Color Palettes",
"version": (1, 4, 1),
"description": "A global color library for Blender with multiple colors per palette, stored in a user-defined JSON file.",
"author": "Jean-Marie Rudent (modified by Gemini)",
"blender": (4, 2, 0),
"location": "Shader Editor > Color Library",
"warning": "",
"category": "Material"
}
import bpy
import json
import os
# -----------------------------
# Data Structures
# -----------------------------
def update_palette(self, context):
context.scene.palettes_modified = True
def update_color_item(self, context):
context.scene.palettes_modified = True
class ColorItem(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="Name", default="Color", update=update_color_item)
color: bpy.props.FloatVectorProperty(
name="Color",
size=4,
min=0.0,
max=1.0,
default=(1.0, 1.0, 1.0, 1.0),
subtype='COLOR',
update=update_color_item
)
class ColorPalette(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="Name", update=update_palette)
colors: bpy.props.CollectionProperty(type=ColorItem)
# -----------------------------
# Addon Preferences
# -----------------------------
class ColorPalettesAddonPreferences(bpy.types.AddonPreferences):
bl_idname = __name__
global_config_path: bpy.props.StringProperty(
name="Global Color Palette File",
subtype='FILE_PATH',
default="",
description="Path to the JSON file storing global color palettes"
)
# -----------------------------
# UI Panel
# -----------------------------
class COLOR_LIBRARY_PT_panel(bpy.types.Panel):
bl_idname = "COLOR_LIBRARY_PT_panel"
bl_label = "Color Palettes"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Color Palettes'
def draw(self, context):
layout = self.layout
scene = context.scene
prefs = context.preferences.addons[__name__].preferences
layout.label(text="Configuration File:")
row = layout.row()
row.prop(prefs, "global_config_path", text="")
row = layout.row()
row.operator("color_library.load_global_config", text="Load Palettes")
layout.separator()
row = layout.row()
row.template_list("COLOR_LIBRARY_UL_list", "", scene, "color_palettes", scene, "color_palette_index")
row = layout.row()
row.operator("color_library.add_palette", text="Add Palette")
row.operator("color_library.delete_palette", text="Delete Palette")
layout.separator()
row = layout.row()
save_text = "Save Palettes"
if scene.palettes_modified:
save_text += " *"
row.operator("color_library.save_global_config", text=save_text)
if scene.color_palette_index >= 0 and len(scene.color_palettes) > 0:
palette = scene.color_palettes[scene.color_palette_index]
layout.prop(palette, "name")
box = layout.box()
for color in palette.colors:
row = box.row(align=True)
row.prop(color, "name", text="")
row.prop(color, "color", text="")
class COLOR_LIBRARY_UL_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
if self.layout_type in {'DEFAULT', 'COMPACT'}:
layout.label(text=item.name)
elif self.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.label(text="")
# -----------------------------
# Operators
# -----------------------------
class COLOR_LIBRARY_OT_add_palette(bpy.types.Operator):
bl_idname = "color_library.add_palette"
bl_label = "Add Palette"
def execute(self, context):
scene = context.scene
new_palette = scene.color_palettes.add()
new_palette.name = f"Palette {len(scene.color_palettes)}"
for i in range(10):
new_color = new_palette.colors.add()
new_color.name = f"Color {i+1}"
new_color.color = (1.0, 1.0, 1.0, 1.0)
context.scene.palettes_modified = True
return {'FINISHED'}
class COLOR_LIBRARY_OT_delete_palette(bpy.types.Operator):
bl_idname = "color_library.delete_palette"
bl_label = "Delete Palette"
def execute(self, context):
scene = context.scene
if scene.color_palette_index >= 0 and len(scene.color_palettes) > 0:
scene.color_palettes.remove(scene.color_palette_index)
scene.color_palette_index = min(scene.color_palette_index, len(scene.color_palettes) - 1)
context.scene.palettes_modified = True
return {'FINISHED'}
class COLOR_LIBRARY_OT_add_color(bpy.types.Operator):
bl_idname = "color_library.add_color"
bl_label = "Add Color"
index: bpy.props.IntProperty()
def execute(self, context):
scene = context.scene
if scene.color_palette_index >= 0 and len(scene.color_palettes) > 0:
palette = scene.color_palettes[scene.color_palette_index]
new_color = palette.colors.add()
new_color.name = f"Color {len(palette.colors)}"
new_color.color = (1.0, 1.0, 1.0, 1.0)
context.scene.palettes_modified = True
return {'FINISHED'}
class COLOR_LIBRARY_OT_load_global_config(bpy.types.Operator):
bl_idname = "color_library.load_global_config"
bl_label = "Load Palettes"
def execute(self, context):
load_global_config_function(context)
context.scene.palettes_modified = False
return {'FINISHED'}
class COLOR_LIBRARY_OT_save_global_config(bpy.types.Operator):
bl_idname = "color_library.save_global_config"
bl_label = "Save Palettes"
def execute(self, context):
save_global_config_function(context)
context.scene.palettes_modified = False
return {'FINISHED'}
# -----------------------------
# Config Load/Save (global)
# -----------------------------
def load_global_config_function(context):
prefs = context.preferences.addons[__name__].preferences
config_file_path = prefs.global_config_path
if not config_file_path:
print("[ColorPalettes] Global config path not set.")
return
if hasattr(bpy.context, 'scene'):
scene = bpy.context.scene
if os.path.exists(config_file_path):
with open(config_file_path, 'r') as file:
try:
config = json.load(file)
scene.color_palettes.clear()
for palette_data in config.get('palettes', []):
new_palette = scene.color_palettes.add()
new_palette.name = palette_data.get('name', '')
for color_data in palette_data.get('colors', []):
new_color = new_palette.colors.add()
new_color.name = color_data.get('name', 'Color')
new_color.color = tuple(color_data.get('color', [1.0, 1.0, 1.0, 1.0]))
print(f"[ColorPalettes] Palettes loaded from: {config_file_path}")
except json.JSONDecodeError as e:
print(f"[ColorPalettes] Failed to load config (invalid JSON): {e}")
except Exception as e:
print(f"[ColorPalettes] Failed to load config: {e}")
else:
print(f"[ColorPalettes] Global config file not found: {config_file_path}")
def save_global_config_function(context):
prefs = context.preferences.addons[__name__].preferences
config_file_path = prefs.global_config_path
if not config_file_path:
print("[ColorPalettes] Global config path not set.")
return
if hasattr(bpy.context, 'scene'):
scene = bpy.context.scene
config = {'palettes': []}
for palette in scene.color_palettes:
palette_data = {
'name': palette.name,
'colors': []
}
for color in palette.colors:
palette_data['colors'].append({
'name': color.name,
'color': list(color.color)
})
config['palettes'].append(palette_data)
try:
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
with open(config_file_path, 'w') as file:
json.dump(config, file, indent=4)
print(f"[ColorPalettes] Palettes saved to: {config_file_path}")
except Exception as e:
print(f"[ColorPalettes] Failed to save config: {e}")
# -----------------------------
# Registration
# -----------------------------
classes = (
ColorItem,
ColorPalette,
ColorPalettesAddonPreferences,
COLOR_LIBRARY_PT_panel,
COLOR_LIBRARY_UL_list,
COLOR_LIBRARY_OT_add_palette,
COLOR_LIBRARY_OT_delete_palette,
COLOR_LIBRARY_OT_add_color,
COLOR_LIBRARY_OT_load_global_config,
COLOR_LIBRARY_OT_save_global_config,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.color_palettes = bpy.props.CollectionProperty(type=ColorPalette)
bpy.types.Scene.color_palette_index = bpy.props.IntProperty(name="Palette Index")
bpy.types.Scene.palettes_modified = bpy.props.BoolProperty(default=False)
# Load config on register (Blender startup if addon is enabled)
if bpy.context:
load_global_config_function(bpy.context)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if hasattr(bpy.types.Scene, "color_palettes"):
del bpy.types.Scene.color_palettes
if hasattr(bpy.types.Scene, "color_palette_index"):
del bpy.types.Scene.color_palette_index
if hasattr(bpy.types.Scene, "palettes_modified"):
del bpy.types.Scene.palettes_modified
if __name__ == "__main__":
register()
Conclusion
J’ai voulu montrer que même sans être développeur Python de formation, il est possible de se débrouiller en utilisant des outils modernes et un peu de jugeotte pour créer des solutions adaptées à ses besoins réels.
Cela dit, si demain j’ai besoin d’un addon beaucoup plus complexe, je pense que je passerai par un humain habile en python.

Comments are closed