====== mesh 2 svg 2 paper ====== Meshlab : https://www.meshlab.net/ Rien tiré de meshlab pour transformer un mesh (stl, obj) en svg \\ Premier essai concluant avec https://www.svgai.org/convert/stl-to-svg, le fichier s'ouvre bien avec inkscape, l'épaisseur des traits est bien trop élevée mais ça s'arrange facilement. __Aucune face n'est cachée__ **Conseil de Laurent : utiliser «ln» de Michael Fogleman** : https://github.com/fogleman/ln C'est programmé en Go, jamais utilisé Pour la suite j'utilise l'objet teapot.obj extrait du [[https://www.cs.utah.edu/~natevm/newell_teaset/newell_teaset.zip|newell_teaset.zip]] {{:recherche:residence_polygones:teapot.obj.png?direct&800|}} ===== Conversion de formats 3D en ligne de commande ===== Avec OpenCTM ( https://sourceforge.net/projects/openctm/ ) sudo apt install openctm-tools Ensuite on peut utiliser **ctmconv** qui permet de convertir les formats suivants : * OpenCTM (.ctm), * Stanford triangle format (.ply), * Stereolitography (.stl), * 3D Studio (.3ds), * COLLADA 1.4/1.5 (.dae), * Wavefront geometry file (.obj), * LightWave object (.lwo), * Geomview object file format (.off), * VRML 2.0 - export only (.wrl). Exemple : ctmconv parasect.obj parasect.stl ===== Infos sur un objet 3D en ligne de commande ===== Nombre de points, de faces, etc. Avec **assimp-utils** sudo apt install assimp-utils assimp info teapot.obj Assimp pour Open Asset Import Library * https://github.com/assimp/assimp * https://the-asset-importer-lib-documentation.readthedocs.io/en/latest/ ===== Affichage d'objets STL ===== Avec GMSH : https://gmsh.info/ qui est aussi capable d'une multitude d'autres choses (en GUI ou CLI) {{:recherche:residence_polygones:gmsh.png?direct&600|}} ===== Installation de Go ===== # ************************************** # installation du langage Go sur Debian 12 @ tenko sudo apt update sudo apt install golang go version # go version go1.19.8 linux/amd64 go env GOPATH # ok : /home/emoc/go ==== Helloworld en Go ==== Créer un fichier vide helloworld.go nano helloworld.go Le fichier helloworld.go contient package main import "fmt" func main() { fmt.Println("HelloWorld, Golang!") } Puis go run hello.go **Comment compiler ce programme pour qu'il puisse être utilisé comme une commande ?** Il faut le transformer en module go mod init example/helloworld # donner un nom et chemin au module go mod tidy # récupérer les dépendances go build -o helloworld # créer le binaire «helloworld» mv ./helloworld ../bin/helloworld Maintenant on peut déclencher la commande avec ~/go/bin/helloworld ===== Utilisation de Simplify ===== Simplify est un logiciel en ligne de commande de Michael Fogleman qui permet de réduire le nombre de faces d'un objet 3D **au format .STL**. Simplify est programmé en Go https://github.com/fogleman/simplify # installer Go (voir ci-dessus) mkdir ~/go/bin go install github.com/fogleman/simplify/cmd/simplify@latest # réduction à 10% des faces de l'objet (652 faces -> 64 faces) ~/go/bin/simplify -f 0.1 parasect.stl parasect-0.1.stl Comparaison (objet original : [[https://models.spriters-resource.com/nintendo_64/pokemonstadium2/asset/287712/|parasect]]) {{:recherche:residence_polygones:parasect_comparaison_reduction_de_faces.png?direct&800|}} ===== Utilisation de ln ===== Pour transformer un objet 3D au format .OBJ en fichier .SVG git clone https://github.com/fogleman/ln.git cd ln go mod init ln/ln go mod tidy placer le fichier teapot.obj dans le dossier et créer le fichier teapot.go : package main import "github.com/fogleman/ln/ln" func main() { scene := ln.Scene{} mesh, err := ln.LoadOBJ("teapot.obj") if err != nil { panic(err) } mesh.UnitCube() scene.Add(ln.NewTransformedShape(mesh, ln.Rotate(ln.Vector{0, 1, 0}, 0.5))) // scene.Add(mesh) eye := ln.Vector{-0.5, 0.5, 2} center := ln.Vector{} up := ln.Vector{0, 1, 0} width := 1024.0 height := 1024.0 paths := scene.Render(eye, center, up, width, height, 35, 0.1, 100, 0.01) paths.WriteToPNG("teapot.png", width, height) paths.WriteToSVG("teapot.svg", width, height) } Puis go run teapot.go Ça marche! Le fichier svg est créé, en fonction du point de vue défini dans le script go, les faces qui doivent l'être sont cachées. **Transformer en exécutable.** \\ La commande est lancée depuis le répertoire courant dans lequel se trouve le fichier teapot.obj, les fichiers résultants (teapot.png et teapot.svg) sont créés dans le répertoire courant. go build -o teapot # construire le binaire mv teapot ../bin/teapot # déplacer dans le dossier ~/go/bin ~/go/bin/teapot # lancer la commande depuis le répertoire courant On obtient {{:recherche:residence_polygones:teapot_dans_inkscape.png?direct&800|}} Extrait du fichier svg ... etc. En manipulant, on dirait bien que les tracés sont doublés ===== obj2svg ===== Je cherche à créer une commande qui soit accessible de n'importe où qui permette de transformer un objet 3D au format .OBJ en image png et fichier SVG __du maillage__ Créer le dossier et le fichier mkdir test_obj2svg cd test_obj2svg touch obj2svg.go # puis l'éditer package main import ( "fmt" "flag" "github.com/fogleman/ln/ln" ) func main() { // Parsing des arguments flag.Parse() args := flag.Args() if len(args) != 1 { fmt.Println("Usage: obj2svg input.obj -> créera 2 fichiers input.obj.png et input.obj.svg") return } pngfilename := args[0] + ".png" svgfilename := args[0] + ".svg" fmt.Printf("pngfilename %s\n", pngfilename) fmt.Printf("svgfilename %s\n", svgfilename) scene := ln.Scene{} fmt.Printf("Loading %s\n", args[0]) mesh, err := ln.LoadOBJ(args[0]) if err != nil { panic(err) } mesh.UnitCube() scene.Add(ln.NewTransformedShape(mesh, ln.Rotate(ln.Vector{0, 1, 0}, 0.5))) // scene.Add(mesh) eye := ln.Vector{-0.5, 0.5, 2} center := ln.Vector{} up := ln.Vector{0, 1, 0} width := 1024.0 height := 1024.0 paths := scene.Render(eye, center, up, width, height, 35, 0.1, 100, 0.01) paths.WriteToPNG(pngfilename, width, height) paths.WriteToSVG(svgfilename, width, height) } Puis go mod init example/obj2svg # initialiser le module go mod tidy # charger les dépendances go run obj2svg.go teapot.obj # ok, tout fonctionne go build -o obj2svg # construire l'exécutable mv obj2svg ../bin/obj2svg # le placer dans le bon dossier # Maintenant on peut exécuter la commande suivante dans n'importe quel dossier ~/go/bin/obj2svg teapot.obj **TODO : permettre la rotation de la vue** ===== rendu wireframe avec blender CLI + gif ===== {{:recherche:residence_polygones:teapot_wire.gif?direct|}} Script python blender à utiliser en ligne de commande avec blender --background --python blender_teapot_wireframe_views.py # Blender 3.4.1 # Debian 12 @ tenko # 20251109, résidence polygones @ Fablab des portes logiques import bpy import math # ------------------------------- # Rendu wireframe "propre" 600x600 # ------------------------------- # Supprimer tous les objets existants bpy.ops.wm.read_factory_settings(use_empty=True) # Importer le STL bpy.ops.import_mesh.stl(filepath="teapot.stl") obj = bpy.context.selected_objects[0] # Supprimer tous les matériaux existants obj.data.materials.clear() # Ajouter un modifier wireframe mod = obj.modifiers.new(name="WireframeMod", type='WIREFRAME') mod.thickness = 0.02 # épaisseur des lignes # Créer un matériau noir shadeless pour le wireframe mat = bpy.data.materials.new(name="WireMat") mat.diffuse_color = (0, 0, 0, 1) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") bsdf.inputs['Base Color'].default_value = (0, 0, 0, 1) bsdf.inputs['Specular'].default_value = 0 bsdf.inputs['Roughness'].default_value = 1 obj.data.materials.append(mat) # Ajouter une caméra cam_data = bpy.data.cameras.new(name="Camera") cam_object = bpy.data.objects.new("Camera", cam_data) bpy.context.collection.objects.link(cam_object) bpy.context.scene.camera = cam_object # Paramètres de rendu scene = bpy.context.scene scene.render.image_settings.file_format = 'PNG' scene.render.resolution_x = 600 scene.render.resolution_y = 600 scene.render.film_transparent = False # fond blanc # scene.render.film_transparent_glass = False # Désactiver l’anti-aliasing # scene.render.use_antialiasing = False scene.render.engine = 'BLENDER_EEVEE' # moteur Eevee plus simple # Eevee anti-aliasing quasi désactivé scene.eevee.taa_render_samples = 1 # Récupérer la scène scene = bpy.context.scene # Créer un monde si nécessaire if scene.world is None: world = bpy.data.worlds.new("World") scene.world = world # Couleur de fond blanc scene.world.use_nodes = True bg = scene.world.node_tree.nodes['Background'] bg.inputs['Color'].default_value = (1, 1, 1, 1) # blanc # Centrer la caméra autour de l'objet center = obj.location # Paramètres rotation n_views = 30 radius = 10 # distance caméra elevation = 5 for i in range(n_views): angle = 2 * math.pi * i / n_views cam_object.location.x = center.x + radius * math.cos(angle) cam_object.location.y = center.y + radius * math.sin(angle) cam_object.location.z = center.z + elevation # Orienter la caméra vers le centre direction = center - cam_object.location rot_quat = direction.to_track_quat('-Z', 'Y') cam_object.rotation_euler = rot_quat.to_euler() # Nom du fichier scene.render.filepath = f"teapot_wire_{i:02d}.png" # Rendu bpy.ops.render.render(write_still=True) Ensuite on peut assembler les images avec convert teapot_wire_*.png -threshold 50% -colors 2 -resize 600x600 teapot_wire.gif {{:recherche:residence_polygones:teapot_facewire.gif?direct|}} Version alternative qui affiche également les faces (et masque les faces cachées) blender --background --python blender_teapot_facewire.py # calculer les rendus d'image convert teapot_facewire_*.png -threshold 50% -colors 2 -resize 300x300 teapot_facewire.gif # préparer l'animation # Blender 3.4.1 # Debian 12 @ tenko # 20251109, résidence polygones @ Fablab des portes logiques # En ligne 65 on peut choisir : fond transparent ou fond monochrome (changement de couleur en ligne 77) import bpy import math # ------------------------------- # Configuration de la scène # ------------------------------- # Supprimer tous les objets existants bpy.ops.wm.read_factory_settings(use_empty=True) # Importer le STL bpy.ops.import_mesh.stl(filepath="teapot.stl") obj = bpy.context.selected_objects[0] # Supprimer tous les matériaux existants obj.data.materials.clear() # ------------------------------- # Matériau blanc pour les faces # ------------------------------- mat = bpy.data.materials.new("FaceWhite") mat.use_nodes = True bsdf = mat.node_tree.nodes["Principled BSDF"] bsdf.inputs['Base Color'].default_value = (1, 1, 1, 1) # blanc bsdf.inputs['Specular'].default_value = 0 obj.data.materials.append(mat) # ------------------------------- # Matériau Wireframe noir # ------------------------------- # Ajouter un modifier wireframe mod = obj.modifiers.new(name="WireframeMod", type='WIREFRAME') mod.thickness = 0.02 mod.use_replace = False # conserve faces originales # Création d’un second matériau pour le wireframe wire_mat = bpy.data.materials.new("WireBlack") wire_mat.use_nodes = True nodes = wire_mat.node_tree.nodes bsdf_wire = nodes.get("Principled BSDF") bsdf_wire.inputs['Base Color'].default_value = (0, 0, 0, 1) # noir bsdf_wire.inputs['Specular'].default_value = 0 obj.data.materials.append(wire_mat) # Associer le modifier wireframe au matériau noir mod.material_offset = 1 # utilise le second matériau # ------------------------------- # Caméra # ------------------------------- cam_data = bpy.data.cameras.new(name="Camera") cam_object = bpy.data.objects.new("Camera", cam_data) bpy.context.collection.objects.link(cam_object) bpy.context.scene.camera = cam_object # Paramètres de rendu scene = bpy.context.scene scene.render.image_settings.file_format = 'PNG' scene.render.resolution_x = 600 scene.render.resolution_y = 600 scene.render.film_transparent = True # False : fond blanc, True : fond transparent scene.render.engine = 'BLENDER_EEVEE' scene.eevee.taa_render_samples = 1 # anti-aliasing minimal # Fond blanc if scene.world is None: world = bpy.data.worlds.new("World") scene.world = world scene.world.use_nodes = True bg = scene.world.node_tree.nodes['Background'] bg.inputs['Color'].default_value = (1, 1, 1, 1) # blanc # ------------------------------- # Paramètres rotation # ------------------------------- center = obj.location n_views = 30 radius = 10 elevation = 5 # ------------------------------- # Générer les images # ------------------------------- for i in range(n_views): angle = 2 * math.pi * i / n_views cam_object.location.x = center.x + radius * math.cos(angle) cam_object.location.y = center.y + radius * math.sin(angle) cam_object.location.z = center.z + elevation # Orienter la caméra vers le centre de l'objet direction = center - cam_object.location rot_quat = direction.to_track_quat('-Z', 'Y') cam_object.rotation_euler = rot_quat.to_euler() # Nom du fichier scene.render.filepath = f"teapot_facewire_{i:02d}.png" # Rendu bpy.ops.render.render(write_still=True) ===== Blender Export Paper Model ===== Un add-on pour Blender permet de «déplier» un objet 3D : [[recherche:residence_polygones:blender_export_paper_model|Export Paper Model]] ===== Utiliser vpype ===== Sur Linux Debian 12, en suivant les indications de https://vpype.readthedocs.io/en/latest/install.html#linux sudo apt-get install pipx pipx ensurepath pipx install "vpype[all]" vpype --version # vpype 1.15.0 vpype random show # ooooooooooooooooooh ! J'ajoute **deduplicate**, plugin vpype pour enlever les lignes en doublon dans un fichier svg https://github.com/LoicGoulefert/deduplicate pipx inject vpype deduplicate vpype --help # pour confirmer que l'installation s'est bien passée : deduplicate apparaît dans la partie Plugins Ainsi que **occult**, plugin vpype pour masquer les faces cachées d'un fichier svg https://github.com/LoicGoulefert/occult pipx inject vpype vpype-occult vpype --help # pour confirmer que l'installation s'est bien passée : occult apparaît dans la partie Plugins Exemple d'utilisation ~/go/bin/simplify -f 0.5 teapot.stl teapot-0.5.stl # simplification de l'objet 3D ctmconv teapot-0.5.stl teapot-0.5.obj # conversion au format OBJ ~/go/bin/obj2svg teapot-0.5.obj # création de 2 fichiers PNG et SVG vpype read teapot-0.5.obj.svg deduplicate write teapot-0.5.obj_dedup.svg # déduplication des arêtes en double dans le fichier SVG ===== Autres trucs intéressants à essayer ===== **removeduplicatelines** : une extension inkscape qui enlève les segments dupliqués : https://cutlings.datafil.no/inkscape-extension-removeduplicatelines/ \\ **vpype** «vpype is an extensible CLI pipeline utility which aims to be the Swiss Army knife for creating, modifying and/or optimizing plotter-ready vector graphics» https://vpype.readthedocs.io/en/latest/install.html#linux