Format STL et Lithophanie

Salut mes p'tits Dyrknautes.

Cela fait plusieurs années que j'utilise une imprimante 3D, et malgré cela je n'avais pas
jusqu'alors pris un instant pour vraiment étudier le format des fichiers que j'utilisais pour obtenir
de magnifiques objets ! Mais il vaut mieux tard que jamais non ?

Ce fichier c'est le format stl ou de son nom plus complet "stéréolithographie".
Pour faire très simple, c'est un format qui repose sur des triangles.
Et par "Triange", je n'entends rien de plus, ni de moins, que la forme géométrique que nous
avons tous découvert lors de notre passage par l'école primaire !

Des triangles qui, mis ensemble, forment un objet en 3 dimensions.

Ce format existe en binaire et en ascii, mais dans cet article, je ne parlerais que du format ASCII.
Cela afin de faciliter l'explication, mais également la compréhension de tous.

Des triangles

Le format STL est donc une série de un ou plusieurs triangles déclaré ainsi :

facet normal 1 0 0
outer loop
vertex 0 0 0
vertex 1 0 0
vertex 1 1 0
endloop
endfacet

facet normal xxxx yyyyy zzzzzz
outer loop
vertex xxxx yyyyy zzzzzz
vertex xxxx yyyyy zzzzzz
vertex xxxx yyyyy zzzzzz
endloop
endfacet

….

Ainsi donc un triangle, commence par la ligne "facet" … cette ligne va fournir des informations sur la "face"
Ce qui n'est pas hyper utile pour l'impression 3D, mais par contre c'est utile pour vous permettre d'apprécier pleinement
la visualisation de votre objet. Gérer les ombrages sur l'affichage à l'écran (par exemple).

Ensuite vient les vertex.
Un triangle à 3 sommets, ce sont ici les "vertex".
Chaque sommets dispose donc d'un coordonnée X, Y et Z.
Et 3 sommets (vertex) forme un triangle.
Ci-dessous une représentation simplifié
De plusieurs triangles format la partie visible d'un cube.

Photo 3D avec la Lithophanie

Maintenant que vous avez compris comment fabriquer de la 3D avec le format stl. Et j'imagine que le cube en illustration aide à la compression.
Je vais vous parler de la lithophanie !

En gros il s'agit de prendre une image, et de l'imprimer en 3D ...
Comment gérer les couleurs me direz vous ?
Nous allons faire comme si l'image était en noir et blanc et utiliser le contraste pour la profondeur.

En parcourant chaque pixel de l'image, un par un. En fonction de la "clarté" du pixel, on fabriquera un petit cube plus ou moins profond sur l'axe Z, qu'on positionnera sur l'axe X, Y de chaque pixel.

Cette différence de profondeur, va permettre une fois imprimé de plus ou moins laisser passer la luminosité, et donc de donner l'impression d'une image en noir et blanc grâce à différentes parties claires et moins claires.

C'est du Minecraft en condition réelle ;)

De la lithophanie à la maison ?

J'ai pour ma part, écrit un petit script maison qui fait ce job.
Il n'est pas parfait, il pourrait être davantage optimisé, car le format STL prends davantage de place que du binaire, et nécessite plus de temps lorsque vous l'ouvrez avec un logiciel, car celui-ci doit le "parser" (en extraire les données et les convertir en valeur décimale).
Néanmoins comme précisé plus haut, il s'agit ici d'un article pédagogique pour permettre à tout le monde de mieux comprendre ce format et d'être en mesure de le manipuler.

Voici le script en question :

html = document.getElementsByTagName('html')[0];
function generateAsciiStlLitophanie(){
    buff = "solid litophanie\n";
    downloadFile = document.createElement('a');
    img = document.getElementsByTagName('img')[0];
    canvas = document.createElement('canvas');
    canvas.width  =  Math.min(img.naturalWidth, 250);
    canvas.height =  (img.naturalHeight * 100 / img.naturalWidth) * canvas.width / 100;
    canvas.ctx = canvas.getContext('2d');
    canvas.ctx.translate(canvas.width, 0);
    canvas.ctx.scale(-1, 1);
    canvas.ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight,
                              0, 0, canvas.width,     canvas.height);
    html.append(canvas);
    createCube = (x, y, z, w, h) => {
    return `facet normal 0 0 -1
outer loop
  vertex ${x} ${y} 0
  vertex ${x+w} ${y} 0
  vertex ${x+w} ${y+h} 0
endloop
endfacet
facet normal 0 0 -1
outer loop
  vertex ${x} ${y} 0
  vertex ${x+w} ${y+h} 0
  vertex ${x} ${y+h} 0
endloop
endfacet
facet normal 0 0 1
outer loop
  vertex ${x} ${y} ${z}
  vertex ${x} ${y+h} ${z}
  vertex ${x+w} ${y+h} ${z}
endloop
endfacet
facet normal 0 0 1
outer loop
  vertex ${x} ${y} ${z}
  vertex ${x+w} ${y+h} ${z}
  vertex ${x+w} ${y} ${z}
endloop
endfacet
facet normal 0 -1 0
outer loop
  vertex ${x} ${y} 0
  vertex ${x} ${y} ${z}
  vertex ${x+w} ${y} ${z}
endloop
endfacet
facet normal 0 -1 0
outer loop
  vertex ${x} ${y} 0
  vertex ${x+w} ${y} ${z}
  vertex ${x+w} ${y} 0
endloop
endfacet
facet normal 0 1 0
outer loop
  vertex ${x} ${y+h} 0
  vertex ${x+w} ${y+h} 0
  vertex ${x+w} ${y+h} ${z}
endloop
endfacet
facet normal 0 1 0
outer loop
  vertex ${x} ${y+h} 0
  vertex ${x+w} ${y+h} ${z}
  vertex ${x} ${y+h} ${z}
endloop
endfacet
facet normal -1 0 0
outer loop
  vertex ${x} ${y} 0
  vertex ${x} ${y+h} 0
  vertex ${x} ${y+h} ${z}
endloop
endfacet
facet normal -1 0 0
outer loop
  vertex ${x} ${y} 0
  vertex ${x} ${y+h} ${z}
  vertex ${x} ${y} ${z}
endloop
endfacet
facet normal 1 0 0
outer loop
  vertex ${x+w} ${y} 0
  vertex ${x+w} ${y} ${z}
  vertex ${x+w} ${y+h} ${z}
endloop
endfacet
facet normal 1 0 0
outer loop
  vertex ${x+w} ${y} 0
  vertex ${x+w} ${y+h} ${z}
  vertex ${x+w} ${y+h} 0
endloop
endfacet
`;
};
    scalePx = 0.4;
    maxHeight = 3*scalePx;
    buff += createCube(-2, -2, 5*scalePx, canvas.width*scalePx+2, canvas.height*scalePx+2);
    for (var y = 0; y <= canvas.height; y++){
        for (var x = 0; x <= canvas.width; x++){
            data = canvas.ctx.getImageData(x, y, 1, 1)
            color = data.data;
            percentColor = ((255-((color[0]+color[1]+color[2])/3))*100/255);
            z = 1 + (maxHeight*(percentColor/100));
            if (color[3] > 0 && z > maxHeight){
                buff += createCube(x*scalePx, y*scalePx, z, scalePx, scalePx);
            }
            grey = ((100-percentColor) * 255 / 100);
            for (var i = 0; i<=2;i++) data.data[i] = grey;
            canvas.ctx.putImageData(data,x, y);
        };
         //break; // to remove after success
    };
    buff +="\nendsolid litophanie";
    //console.log(buff);
    downloadFile.href = URL.createObjectURL(new Blob([buff], {contentType:'text/csv'}));
    downloadFile.target = 'BLANK'
    downloadFile.download='lithophanie.stl';
    downloadFile.click();
};
html.textContent = '';
img = document.createElement('input');
img.type = 'file';
html.appendChild(img);

html.appendChild(document.createElement('p'));
img.addEventListener('change', (e)=>{
    let tmpImg = document.createElement('img'), 
        imgBlob = new FileReader();
    html.appendChild(tmpImg);
    tmpImg.setAttribute('style', 'width:250px');
    tmpImg.addEventListener('load', generateAsciiStlLitophanie);
    imgBlob.addEventListener('load', (evt)=>{
        console.log(evt.target.result);
        tmpImg.src = evt.target.result;
    });
    imgBlob.readAsDataURL(e.target.files[0]);
});

(1) Copiez ce code, puis dans n'importe quel onglet de votre navigateur, ouvrez la console développeur (Touche F12 du clavier, ou clique droit > "inspecter").
(2) Depuis la console développeur, rendez-vous sur l'onglet "console"
(3) Collez le script dedans, puis validez avec la touche "entrée"

A présent vous devriez voir apparaitre un bouton sur la page où vous avez fait cette manipulation.
Ce bouton vous permettra de charger une image et de vous générer directement un fichier stl.
Que vous pourrez imprimer, ou bien si vous êtes simplement curieux, de le visualiser (Depuis votre moteur de recherche, vous pouvez chercher "visionneuse stl en ligne")

Comme indiqué plus haut, ce script va parcourir chaque pixel de votre image et les "convertir" en cube de différente taille selon leurs couleurs.

Voici un test que j'ai réalisé chez moi.

Fichier d'impression 3D et risques

Les imprimantes 3D actuelles sont de plus en plus connectées, avec de plus en plus de capteurs, des caméras pour observer votre impression (et votre maison ?)
Soyez donc curieux ... mais surtout extrêmement vigilant sur les fichiers que vous allez fournir à votre imprimante.

Ici j'ai choisi de vous présenter le format STL sous sa forme "ASCII" (lisible), cependant c'est probablement plus facile à comprendre, mais c'est moins optimisé, et surtout plus ouvert pour l'exploitation de faille de type buffer overflow (débordement de mémoire), car il va y avoir un gros travail à faire pour extraire de chaque ligne les données x, y et z et les convertir numériquement.
Dans le format binaire les données sont à la suite les unes des autres, il n'y a pas d'espace, de tabulation, etc ... et les données x, y et z sont "encodées" sur 2 octets, alors que dans le format ascii ... on peut créer des valeurs sur des kilomètres

vertex 1111111111111111111111.....20 kilomètres plus tard ....11111111111

Pour l'anecdote sur la sécurité :
Certaines imprimantes "connectées", téléchargent localement ou à distance votre fichier à imprimer ... puis le passe directement en paramètre dans une en ligne de commande ...
Sans aucun contrôle préliminaire de sécurité sur le nom du fichier ... (celui-ci pouvant contenir dans son nom, une ou plusieurs commandes malveillantes).
Et je ne dis pas ça parce que je l'ai lu ... j'ai pu l'expérimenter sur des imprimantes très récentes.

L'obj un format similaire.

Le format STL est plutôt bien pour des représentations 3D statiques.
Néanmoins il existe des formats beaucoup plus adaptés à l'animation et aux jeux vidéos.
Le format OBJ par exemple, celui-ci repose (en partie) sur le même principe que le format STL, il ne vous sera pas difficile de l'assimiler si vous avez compris cet article :
https://fr.wikipedia.org/wiki/Objet_3D_(format_de_fichier)

Conclusion

J'espère que cet article vous aura donné envie de faire un peu de modélisation 3D.
J'avais récemment fait un article sur {FreeCad} Comment trouver une 0day ! qui est un outil
que j'utilise (entre autre) pour faire des modélisations.
Cet article a vocation de vous faire connaitre un peu ce format, car à mes yeux il est important de mieux
comprendre ce que l'on fait :
A la fois pour être capable de se débrouiller seul en cas de problème.
Mais aussi pour mettre plus en perspective la notion de "risque".

Laisser une réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site est protégé par reCAPTCHA et Google Politique de confidentialité et Conditions d'utilisation appliquer.