Estrusione di Shape SVG in direzioni arbitrarie e tagli ad angolo in Three.js

Introduzione

In alcuni contesti specifici, come il trattamento di elementi meccanici provenienti da progettazione CAD, si rende necessario visualizzare a schermo il risultato di una estrusione di tale tracciato così da poterne vedere in tre dimensioni il risultato di lavorazione finale. Contesti più specifici necessitano di estrudere tali elementi in direzioni arbitrarie mantenendone l’uniformità, così come altri necessitano di giunzioni con tagli ad angolo per creare strutture più complesse… pensiamo ad esempio alle cornici dei quadri.

Per il test di questo articolo è stata utilizzata una Shape rappresentate una semplice forma di cuore, utile soprattutto per testare l’approccio in contesti di curve e archi.

Caricamento di file SVG sotto forma di Shape singole

L’oggetto THREE.Shape (derivato da THREE.Path) è un oggetto che permette di disegnare componenti 2D vettoriali nel canvas 3D di Three.js.
Sebbene sia uno strumento molto potente è anche molto complesso da utilizzare quindi il suo uso più frequente è quello di trovarlo implementato nel THREE.SVGLoader. Tale loader estrapola dal file .svg oggetti Path e Shape così da poterli usare per generare la geometria desiderata.

Qui sotto una procedura standard per ottenere le singole Shape dell’SVG che sfrutta la funzione toShape() delle Path:

 const loader = new THREE.SVGLoader();
        loader.load(
            'heart.svg',
            function (data) {
                const paths = data.paths;
                let counter = 0;
                for (let i = 0; i < paths.length; i++) {
                    const shapes = paths[i].toShapes(true);
                    counter = counter + shapes.length;
                    for (let j = 0; j < shapes.length; j++) {
                        let newShape = shapes[j]resize; //Shape corrente
                        //Operazioni sulla shape
                        //...
                    }
                }
            },

            // called when loading is in progresses
            function (xhr) {
                console.log((xhr.loaded / xhr.total * 100) + '% loaded');
            },
            // called when loading has errors
            function (error) {
                console.log(error);
            }
        );

Normalizzazione delle dimensioni della Shape

Uno dei problemi che si deve affrontare durante il caricamento di una Shape è quello che l’unità di misura della Shape può essere diversa rispetto a quella della scena in cui dobbiamo includerla. Per questo motivo si rende necessario normalizzare le dimensioni della Shape (e non della Geometry o della Shape successive).

Una tecnica è quella di utilizzare la funzione Shape.getSpacedPoints(…) che permette di ottenere tutti i punti che compongono la Shape e quindi rimapparli con un fattore di scala unico oppure con dimensioni desiderate.

function GetShapeResized(shape, targetWidth, targetHeight)
{
    const boundingBox = new THREE.Box2();
    boundingBox.setFromPoints(shape.getSpacedPoints(100)); 

    const originalWidth = boundingBox.max.x - boundingBox.min.x;
    const originalHeight = boundingBox.max.y - boundingBox.min.y;

    const scaleX = targetWidth / originalWidth;
    const scaleY = targetHeight / originalHeight;

    const points = shape.getSpacedPoints(100);
    const resizedPoints = points.map(point => {
        return new THREE.Vector2(point.x * scaleX, point.y * scaleY);
    });

    const resizedShape = new THREE.Shape(resizedPoints);
    return resizedShape;
}

A questo link del blog sono spiegate in dettaglio le 2 tecniche utilizzabili.

Generazione di una geometria unificata

Come detto un file SVG può essere composto da molte Shape e non da una sola Shape. Per questo motivo si rende necessario, al termine del caricamento e della normalizzazione, la creazione di un unico oggetto Geometry e non diversi oggetti, in quanto sarebbero dispendiosi e poco controllabili.

La funzione che ci viene in aiuto, durante il caricamento delle Shape dell’SVG, è la seguente:

const result = THREE.BufferGeometryUtils.mergeBufferGeometries(geometriesArray);

Tale funzione permette di unire in un unica geometria tutte le geometrie presenti nell’array di geometrie passato alla funzione. Tale oggetto può essere poi facilmente convertito in Mesh e aggiunta alla scena corrente che la renderizzerà a video.

Creazione di una estrusione lungo direzioni arbitrarie basate su punti

Three.js implementa di base la funzione THREE.ExtrudeGeometry che permette di estrudere una Shape. Uno dei parametri che si possono passare alla funzione è la path lungo cui estrudere la forma. Sebbene questa sia una funzionalità molto interessante soffre di due problemi principali: crea un’unica geometria finale e non è possibile generare geometrie chiuse. Seppur utile i contesti di applicazione sono molto ristretti spesso a demo o semplici trasformazioni di oggetti da 2D a 3D spessorato.

L’obiettivo è invece quello di implementare una funzione che permetta, passando la forma della Shape e una serie di punti 2D, di estrudere la forma nella direzione di quei punti nello spazio, poter decidere se le componenti geometriche sono separate e implementare un metodo per chiudere l’estrusione generata (affinché non vi sia un sormontare tra la prima geometria e l’ultima). Esistono diversi approcci a questo tipo di attività e vanno tutti nell’usare sapientemente gli indici della geometria calcolando l’angolo tra i bordi del contorno e applicando matrici di taglio/trasformazione, rotazione e traslazione ai punti del profilo per ciascun punto del contorno.

for (let i = 0; i < this.pathPoints.length; i++)
{
    let angle1 = 90;    //Arbitrario
    let angle2 = 45;    //Arbitrario

    let cutAngle = angle2 - angle1;
    let oppositeAngle = (Math.PI * 0.5) + angle2;

    let side = new THREE.Matrix4().set(1, 0, 0, 0, -Math.tan(cutAngle * 0.5 - Math.PI * 0.5), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
    THREE.ShapeGeometry(shape).attributes.position.applyMatrix4(side);

    let rotation = new THREE.Matrix4().set(Math.cos(oppositeAngle), -Math.sin(oppositeAngle), 0, 0, Math.sin(oppositeAngle), Math.cos(oppositeAngle), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
    THREE.ShapeGeometry(shape).attributes.position.applyMatrix4(rotation);

    let move = new THREE.Matrix4().set(1, 0, 0, pathPoints[i].x, 0, 1, 0, pathPoints[i].y, 0, 0, 1, 0, 0, 0, 0, 1);
    THREE.ShapeGeometry(shape).attributes.position.applyMatrix4(move);
}
Video dei risultati ottenuti con la tecnica dell’estrusione in direzioni arbitrarie

Tagli ad angoli arbitrari

Come visto il passaggio precedente crea estrusioni lungo direzioni arbitrarie, anche di geometrie che devono essere necessariamente chiuse. E’ possibile però specificare alla funzione di produrre unicamente la Shape estrusa originaria ed utilizzare le estrusioni secondarie unicamente come elemento di rimozione dei punti eccedenti e quindi generare tagli ad angoli arbitrari.

L’espediente che possiamo usare è quantomai semplice: si può definire un booleano che nei cicli di generazione degli indici della geometria risultante escluda quelli che generano la componente grafica dell’estrusione ad angolo. Otterremo così un profilo estruso in due direzioni ma di cui la seconda parte della geometria non viene renderizzata. In sostanza stiamo usando le geometrie estruse unicamente per “scavare” la geometria di interesse. Questo approccio permette di non dover riscrivere la funzione del punto precedente ma invece di riutilizzarla per questo contesto. Seppur con qualche calcolo di troppo risulta comunque la soluzione vincente in termini di implementazione e di riusabilità del codice.

Qui sotto un taglio a 45° in cui viene mostrata, unicamente per debug, l’estrusione usata per generarlo:

Qui sotto due semplici esempi ottenuti con un taglio da 45° e uno da 30°:

Specchiatura della Mesh risultante (se necessario)

Per poter utilizzare la Mesh prodotta anche in situazioni dove il profilo deve trovarsi in situazione specchiata è necessario attuare una semplice operazione di inversione dei punti. L’approccio più semplice è quello di usare un espediente basato sulla scalatura al quale viene passato una unità con valore negativo, sull’asse di specchiatura desiderato.

La semplice funzione da utilizzare è la seguente (specchiatura su asse X):

mesh.scale.multiply(new THREE.Vector3(-1,1,1));

Considerazioni sulla velocità di esecuzione

E’ chiaro che parlando di Three.js e rendering realtime sul web sia strettamente necessario tenere in considerazione la velocità di esecuzione di operazioni così a basso livello. Proprio perchè operazioni a basso livello (siamo partiti da un file SVG caricato da disco, passando da oggetti Path e Shape, da geometrie ricalcolate, estrusioni e tagli arbitrari) esse sono altamente performanti.

Dai nostri test il caricamento di un .svg semplice da disco con la creazione di un riquadro sagomato tramite Shape a forma di cuore ha impiegato tra i 10 e i 15 millisecondi in totale contemplando il caricamento del file, la generazione delle Shape e delle geometrie, la normalizzazione, l’estrusione arbitraria e il taglio di 4 oggetti (uno per lato, colorati nell’immagine sottostante).

Shape estrusa attraverso punti arbitrari (sinistra) e Mesh composta da 4 geometrie estruse e tagliate ad angolo (destra)

https://www.desdinova.it
Aiuto aziende e professionisti che hanno bisogno di sviluppare in modo creativo, alternativo ed efficace la loro identità digitale e che desiderano ottenere visibilità e risultati concreti attraverso lo sviluppo di strumenti online dall'elevata innovazione e personalizzazione (3D, Realtà Virtuale, Realtà Aumentata, Advergame, etc)
Daniele Ferla
Aiuto aziende e professionisti che hanno bisogno di sviluppare in modo creativo, alternativo ed efficace la loro identità digitale e che desiderano ottenere visibilità e risultati concreti attraverso lo sviluppo di strumenti online dall'elevata innovazione e personalizzazione (3D, Realtà Virtuale, Realtà Aumentata, Advergame, etc)

Must Read