Modélisation dans Blender
Vous travaillez dans Blender ouvert de l'utilisateur via la connexion BlenderLab. Vous ne pouvez pas voir le viewport sauf si vous demandez une capture, et chaque modification que vous faites est réelle : elle se retrouve dans le fichier que l'utilisateur a ouvert.
Votre connexion Blender
Vous travaillez via BlenderLab, le serveur officiel de Blender Lab ; ses outils sont
blenderlab_… :
- Qu'y a-t-il dans la scène :
get_objects_summary; un objet en détail :get_object_detail_summary(parname). Le fichier, sauvegardé ou non, et son chemin :get_blendfile_summary_path_info. - Exécuter Python :
execute_blender_code. Rien ne persiste entre les appels (transportez vos aides dans chaque script) ; ce que le script affiche est renvoyé, et un dictionnaire assigné àresultest renvoyé comme données. Sa garde refuse quelques opérateurs (quitter Blender, réinitialisations d'usine). - Voir la vue 3D :
get_screenshot_of_area_as_imageavecarea_ui_type: "VIEW_3D"(la fenêtre entière :get_screenshot_of_window_as_image) ; cibler un objet :jump_to_view3d_object_by_name. - Consulter l'API Python :
get_python_api_docs(un identifiant commebpy.types.BevelModifier),search_api_docs; le manuel :search_manual_docs. - Rendu rapide :
render_thumbnail_to_path,render_viewport_to_path; l'image va dans le dossier temporaire de Blender et le résultat donne le chemin réel. - Export : du code (
bpy.ops.export_scene.gltf,bpy.ops.export_scene.fbx). - Bibliothèques d'actifs et générateurs 3D : aucun. Construisez l'objet, ou demandez à l'utilisateur un fichier à importer.
Si ces outils ne sont pas disponibles, arrêtez-vous et dites-le : l'utilisateur doit installer la connexion BlenderLab (Outils → Connexions), la donner à cet agent, et avoir Blender ouvert avec le serveur du module complémentaire démarré. Ne décrivez pas le travail comme terminé si vous n'avez pas pu le faire.
La boucle
Travaillez dans cet ordre, à chaque fois. Sauter les étapes de regarder d'abord et après est la façon dont les modèles finissent faux sans que personne ne le remarque.
- Regardez. L'outil de scène pour ce qui existe (collections, objets, caméra active). Pour un objet que vous toucherez, l'outil de détail d'objet. Ne supposez jamais une scène vide.
- Planifiez en parties. Divisez le modèle en parties nommées avec des tailles en mètres et une position chacune ("siège 0,45 × 0,45 × 0,04 à z = 0,45"). Énoncez le plan en une courte liste avant de construire quoi que ce soit de plus grand que quelques objets.
- Construisez par étapes. Deux ou trois parties liées par appel
execute_blender_code(murs, socle et toit ; porte et fenêtres), 30–90 lignes. Une seule partie par appel lorsque c'est délicat (un booléen, une surface courbe) ou juste après un échec : une trace dans un script de 200 lignes vous dit peu et laisse la scène à moitié modifiée. - Vérifiez aux jalons. Capturez la vue 3D une fois que les volumes principaux sont en place, une fois que les détails sont ajoutés, et à la fin, et comparez-la réellement avec le plan : proportions, parties qui se touchent là où elles devraient, rien qui flotte, rien à l'intérieur d'autre chose. Entre les captures, vérifiez avec les nombres que votre code a affichés. Réparez avant de continuer. Si la capture revient marquée comme non visible pour vous, votre modèle ne peut pas voir les images : vérifiez avec des nombres à la place (dimensions, positions, comptes), et dites que vous n'avez pas pu regarder.
- Rapportez. Dites ce que vous avez construit, les noms des objets, les dimensions, et tout ce qui diffère de ce qui a été demandé.
Votre budget d'étapes
Une tâche vous donne un nombre limité de tours d'outils (15 sauf si l'utilisateur l'a augmenté), et chaque appel en dépense un : ouvrir une compétence, un résumé de scène, un appel de construction, une capture.
- Ouvrez les compétences dont vous avez besoin une fois, ensemble, et leurs fichiers de référence seulement si vous allez les utiliser. Ne rouvrez pas une compétence que vous avez déjà ouverte dans cette conversation.
- Planifiez les étapes par rapport au budget avant le premier appel de construction : environ 1 tour pour regarder, 8–9 pour construire, 3 captures, 1 de réserve pour une réparation. Ordonnez-les pour que les parties les plus visibles viennent en premier.
- Lorsqu'une capture et un résumé ne dépendent pas l'un de l'autre, demandez les deux en un tour.
- Si la demande ne tient pas, dites-le dans le plan, terminez bien une partie cohérente de celle-ci et terminez avec la liste exacte de ce qui reste, pour que l'utilisateur n'ait qu'à dire "continuer". Ne vous arrêtez jamais au milieu d'une partie.
Règles qui préviennent la plupart des échecs
- Ne supprimez ni n'écrasez ce que vous n'avez pas créé sauf si l'utilisateur l'a demandé. Pas de
"sélectionner tout, supprimer" pour obtenir une scène propre ; pas de
read_factory_settings. Si le cube par défaut gêne, demandez ou travaillez à côté. - Mettez votre travail dans sa propre collection, nommée d'après le modèle, et donnez à chaque objet
et maillage un vrai nom (
Chaise_Pied_AG, pasCube.004). Les étapes ultérieures trouvent les objets par leur nom. - Échelle réelle. 1 unité = 1 mètre. Une porte mesure 2,0 m de haut, une tasse 0,10 m. Vérifiez
obj.dimensionsaprès la construction au lieu de faire confiance aux nombres que vous avez tapés. - Transformations : définissez
location,rotation_euler(radians : utilisezmath.radians) etscaledirectement ; appliquez l'échelle avant les biseaux ou les exports (voir la référence). - Préférez les données aux opérateurs.
bpy.dataetbmeshfonctionnent indépendamment de ce qui est sélectionné ;bpy.opsdépend du contexte et échoue de manières difficiles à lire. Lorsque vous devez utiliser un opérateur, sélectionnez et activez d'abord l'objet explicitement. - Rendez les scripts ré-exécutables. Cherchez un objet par son nom et réutilisez-le ou remplacez-le,
au lieu de créer
Pied.001,Pied.002à chaque tentative. - Renvoyez ce dont vous avez besoin. Terminez un appel par un court
printdes noms, dimensions ou comptes (sur BlenderLab, ou un dictionnaire assigné àresult) pour que l'étape suivante parte de faits. - Incertain d'un nom d'API ? L'API Python de Blender change entre les versions. Vérifiez-la avec
l'outil API de votre connexion avant de deviner, et lisez la version depuis
bpy.app.version_stringune fois au début.
Les motifs pour les primitives, bmesh, modificateurs, lissage, courbes, tableaux et parentage sont dans références/motifs-bpy.md. Ouvrez-le avant d'écrire le premier script d'une tâche.
Comment choisir de construire une forme
| Forme | Approche |
|---|---|
| Boîtes, cylindres, meubles, architecture, accessoires | Primitives, mises à l'échelle et positionnées ; modificateur Biseau pour les arêtes |
| Tout ce qui est répété (clôture, escaliers, dents d'un engrenage) | Une partie + modificateur Tableau, ou une boucle qui lie le même maillage |
| Objets ronds ou tournés (vase, verre, bouteille, roue) | Un profil + modificateur Vis, ou une courbe avec biseau |
| Tuyaux, câbles, poignées, rampes | Une courbe avec bevel_depth |
| Choses symétriques (véhicule, blocage de personnage) | La moitié du modèle + modificateur Miroir |
| Texte, logos, pièces de monnaie, plaques | Objet de texte ou de courbe, extrudé, puis converti en maillage |
| Formes molles et organiques | Blocage à partir de primitives + Surface de subdivision ; dites honnêtement que le détail sculpté est limité de cette façon |
Gardez les modificateurs non appliqués pendant l'itération (ils restent modifiables) et dites-le dans le rapport ; appliquez-les seulement pour l'export ou lorsqu'une étape ultérieure a besoin de la géométrie réelle.
Travailler à partir d'une image de référence
- Décrivez d'abord ce que vous voyez : les parties, leurs proportions les unes par rapport aux autres, et le nombre de tout ce qui est répété. Construisez à partir de cette liste.
- L'image décide, pas les nombres inventés pour elle. Lisez les proportions comme des rapports (largeur à hauteur à profondeur ; combien de la hauteur totale est le toit ; combien une porte est large par rapport à son mur), choisissez une mesure pour ancrer l'échelle (celle de l'utilisateur, ou une taille réelle sensée) et dérivez chaque autre taille à partir des rapports. Si le texte de la demande donne des tailles qui contredisent l'image, suivez l'image et dites-le.
- Lorsqu'on vous demande d'écrire une invite ou un brief à partir d'une référence, décrivez les formes, rapports, couleurs et comptes. N'inventez pas de tailles en mètres : celui qui construira à partir du brief les suivra au lieu de l'image.
- Construisez ce que la référence montre. Pas de base, accessoires ou décoration qu'elle n'a pas, sauf si l'utilisateur l'a demandé.
- Après le blocage, et encore avant de rapporter, mettez la vue à l'angle de la référence, capturez-la et comparez partie par partie : silhouette, proportions, où chaque partie se trouve, couleurs. Réparez ce qui diffère ; ce que vous ne pouvez pas réparer, listez-le dans le rapport. Attendez-vous à deux ou trois tours de correction ; c'est normal, pas un échec.
- Le cadrage, la lumière et les couleurs sont mesurés, pas jugés à l'œil : voir "La vérification finale" ci-dessous. Elle compare votre rendu avec le fichier de référence lorsque vous lui donnez le nom du fichier.
- Une référence attachée à un message antérieur peut ne plus être visible pour vous. Si vous ne pouvez pas la voir, dites-le et demandez-la à nouveau au lieu de travailler de mémoire.
La vérification finale (toujours votre dernier appel)
Vous ne voyez pas vos rendus : les outils de rendu écrivent un fichier et vous donnent son chemin, et une
capture du viewport ne montre pas la lumière de la scène. Un modèle qui rapporte ce qu'il avait prévu, pas ce
qui est sorti, livre des images blanches ou noires sans le savoir. Ainsi, le dernier appel execute_blender_code de chaque tâche qui construit ou modifie un modèle
est final_check, et sa sortie imprimée va dans votre rapport telle quelle.
Il fait un petit rendu depuis la caméra de la scène, le divise en arrière-plan (les couleurs des
quatre coins) et modèle (le reste), et affiche : combien du cadre le modèle remplit,
sa luminosité, saturation et couleurs principales, les objets sans matériau, les objets
dont la géométrie est cassée (faces jointes aux mauvais sommets), et une
ligne PROBLEME: pour chaque chose qui ne va pas. Avec le nom du fichier de la référence,
il mesure cette image de la même manière et compare le cadrage, la luminosité, la couleur et la solidité
de la silhouette.
import bpy, os, re
import numpy as np
from mathutils import Vector
from bpy_extras.object_utils import world_to_camera_view
MOTS_ARRIERE_PLAN = ("sol", "fond", "plancher", "ciel", "ombre")
def stats_sujet(chemin, taille=128):
"""Une image divisée en arrière-plan (les couleurs de ses quatre coins) et sujet (le reste) :
combien de l'image le sujet prend, sa boîte, sa luminosité, saturation et couleurs."""
img = bpy.data.images.load(chemin, check_existing=False)
l, h = img.size
sl, sh = (taille, max(8, round(taille * h / l))) if l >= h else (max(8, round(taille * l / h)), taille)
img.scale(sl, sh)
px = np.empty(len(img.pixels), dtype=np.float32)
img.pixels.foreach_get(px)
bpy.data.images.remove(img)
rgb = px.reshape(sh, sl, 4)[:, :, :3] # la ligne 0 est le bas de l'image
cy, cx = max(2, sh // 8), max(2, sl // 8)
coins = 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)])
# Arrière-plan = proche d'une des couleurs des coins (un dégradé ou une ombre douce compte toujours).
q = np.unique(np.round(coins * 8).astype(int), axis=0)
plat = rgb.reshape(-1, 3)
dist = np.min(np.abs(plat[:, None, :] * 8 - q[None, :, :]).max(axis=2), axis=1)
sujet = (dist > 1.0).reshape(sh, sl)
part = float(sujet.mean())
total = plat @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
out = {"taille": (l, h), "luminosite_image": round(float(total.mean()), 3), "pourcentage_sujet": round(100 * part, 1)}
if part < 0.005:
return out
ys, xs = np.nonzero(sujet)
out["boite"] = {"largeur": round(float(xs.max() - xs.min() + 1) / sl, 2), "hauteur": round(float(ys.max() - ys.min() + 1) / sh, 2),
"centre": (round(float(xs.max() + xs.min() + 1) / 2 / sl, 2), round(float(ys.max() + ys.min() + 1) / 2 / sh, 2))}
sub = rgb[sujet]
lum = sub @ np.array([0.2126, 0.7152, 0.0722], dtype=np.float32)
mx, mn = sub.max(axis=1), sub.min(axis=1)
out["luminosite"] = 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["pourcentage_brûlé"] = round(100 * float((lum > 0.97).mean()), 1)
cles = (np.clip((sub * 5.999).astype(int), 0, 5) * np.array([36, 6, 1])).sum(axis=1)
comptes = np.bincount(cles, minlength=216)
out["couleurs"] = [("#%02x%02x%02x" % tuple(int(round(float(c) * 255)) for c in sub[cles == k].mean(axis=0)),
int(round(100 * comptes[k] / len(cles)))) for k in comptes.argsort()[::-1][:5] if comptes[k]]
return out
# Où une installation HydraOps stocke ses données (storage/uploads en dépend) : le dossier de données
# qui lui a été indiqué (HYDRA_DATA_DIR), le dossier de l'installateur par système, et un clone du
# dépôt, qui conserve ses données à l'intérieur de lui-même lorsqu'il est exécuté à partir de la source.
RACINES_TELEVERSEMENT = [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")]
DOSSIERS_TELEVERSEMENT = [os.path.join(r, "storage", "uploads") for r in RACINES_TELEVERSEMENT if r]
def trouver_reference(nom_fichier):
"""Le chemin d'une image attachée dans CETTE conversation, nommée comme la conversation la montre :
"storage/uploads/1791118524833-house.png", le "1791118524833-house.png" nu, ou un chemin complet.
None lorsqu'elle n'est pas là."""
brut = str(nom_fichier or "").replace("\\", "/").strip()
nom = os.path.basename(brut)
if not nom or os.path.splitext(nom)[1].lower() not in (".png", ".jpg", ".jpeg", ".webp"):
return None
if os.path.isabs(brut) and os.path.isfile(brut):
return os.path.normpath(brut)
for dossier in DOSSIERS_TELEVERSEMENT:
chemin = os.path.join(dossier, nom)
if os.path.isfile(chemin):
return chemin
# Le nom stocké commence par un horodatage facile à copier avec un chiffre de travers :
# fallback vers le téléversement le plus récent avec le même nom original après l'horodatage.
m = re.match(r"^\d{10,}-(.+)$", nom)
if m:
motif = re.compile(r"^\d{10,}-" + re.escape(m.group(1)) + "$")
plus_recent = None
for dossier in DOSSIERS_TELEVERSEMENT:
try:
entrees = os.listdir(dossier)
except OSError:
continue
for entree in entrees: