Striscia
v. 1.01
un'utility gratuita ed aperta per il POV-Ray™ v. 3.1

di Daniele Varrazzo
(w) 1997-1999 by PiroSoftware c.n.f.

Homepage: Punto triplo

Se hai suggerimenti, nuove idee o hai trovato un bug scrivi a piro@officine.it


    Indice


[

Mi sono trovato piuttosto in imbarazzo nello scrivere questa procedura. Ho avuto da subito intenzione di pubblicarla, ed è la prima cosa che pubblico. Mi sarebbe piaciuto scriverla in italiano, ma ci ho riflettuto sopra per un po'. Sono arrivato a queste conclusioni...

Il POV è scritto in inglese. Sono inglesi i manuali, i sorgenti, il linguaggio. Sostanzialmente chiunque usi il POV conosce l'inglese, anche se non è un anglofono, come non lo sono io.

Inoltre già da tempo do nomi inglesi alle variabili nei programmi che scrivo. Lo faccio perché trovo più facile meccanizzare l'inglese, che con i suoi get/put in/out read/write mi facilita un codice asciutto e coerente. Come esprimereste CreateLookupTable in italiano? CreaTabellaSguardo? FammiUnaTavolaDiSbirciata?! Naaa...

È anche inutile sparare messaggi d'errore in italiano a un americano che conosce solo la pizza e il mandolino. Ma anche a un francese uno spagnolo o un giapponese... non che voglia conquistare il mondo con una routine 'manco troppo originale, ma vorrei farmi capire.

Insomma è quasi tutto in inglese. Con un pizzico vezzoso di italiano: sono in italiano i nomi degli oggetti che vengono generati. Il mio oggetto si chiama "striscia", non me ne frega se qualcuno non capisce cosa voglia dire la parola. Penso comunque che chiunque usi il POV senza interfaccia abbia una mentalità abbastanza aperta da accettare qualche compromesso...

Vabbuo', al lavoro!

]


Una definizione "vecchio stile" di superficie è:

"il luogo dei punti di una curva che viene traslata e contemporaneamente deformata".

Io ho intenzione di darvi esattamente questo: prenderete tra le mani una curva e la sposterete nello spazio. Potreste prendere una forma semplice, come un hula-hop giallo e cominciare a camminare spostandolo.

Immaginate di essere ripresi da una macchina fotografica a lunga esposizione: ciò che vedrete nella foto sarà una superficie gialla, una specie di "tubo". Se avete scelto di camminare in linea retta, avrete ottenuto la superficie laterale di un cilindro. Se avete girato su voi stessi ci sarà un "toro", una ciambella.

Io vi darò la possibilità di stringere tra le mani qualunque curva vogliate. E di percorrere tutta la strada che volete.

E molto altro.


Prima di fare qualunque cosa, create una nuova scena ed aggiungete la riga

#include "Striscia.inc"

"Striscia.inc" e gli altri file che avete trovato devono risiedere in una directory indicata da un percorso di libreria.

Ora bisogna imparare a descrivere le curve, è necessario prima di continuare. Se avete già usato gli oggetti lathe e prism del POV, allora già sapete quasi tutto.

Avete la possibilità di descrivere le curve con le stesse regole con cui descrivete la forma della base di un prisma: potrete fare una curva che sia lineare a tratti, o una cubica espressa con i punti di passaggio o con il poligono di Bezier. Non vi farò usare il sistema quadrico: lo considero inutile e difficile da controllare.

Ho scritto delle macro che restituiscono una curva come se fosse un oggetto: voi potrete ad esempio assegnarla ad una variabile.

La funzione più semplice è quella che crea una curva lineare a tratti, cioè una linea spezzata. Si richiama con...

splLinearSpline(array[n] {v1, v2, ...., vn})

i punti da v1 a vn sono dei vettori 3D. Questa è la differenza sostanziale rispetto alle curve di controllo degli oggetti prism e lathe, che devono essere per forza definite sul piano z=0.

Questo esempio rappresenta un quadrato contenuto in parte nel piano orizzontale (il piano XY), che ad un certo punto si piega e si "alza" lungo l'asse z:

#declare splQuadrato = splLinearSpline(array[6] {
   <-1, 0, -1>, <1, 0, -1>, <1, 0, 1>, <0, 0, 1>, <-1, 1, 1>, <-1, 1, -1>
})

Non va usato il ";" alla fine dell'istruzione. Non è necessario neanche che la curva sia chiusa: se non lo fate, io non penserò a chiuderla per voi. Potete benissimo generare una curva aperta, questo è un paese libero.

Volete vedere l'aspetto della vostra curva? Bene: ho preparato una macro per voi. Aggiungete alla scena la riga:

PlotSpline(splQuadrato, <0.75, 2, -2.5>)

dove il vettore che segue il nome della spline da plottare è il punto di vista dell'osservatore. Il risultato sarà:

Una curva sghemba

Se definite una curva giacente nel piano XY, potete plottarla usando la macro PlotXYSpline(splSpline). Per esempio:

#declare splPiatta = splLinearSpline(array[5] {
   <-1, -1, 0>, <-2, 2, 0>, <-1, 4, 0>, <3, 1, 0>, <1, -1, 0>
})
PlotXYSpline(splPiatta)

Una curva nel piano XY

Potete plottare più curve contemporaneamente. Per esempio se aggiungete alla scena precedente le righe:

#declare splSeconda = splLinearSpline(array[6] {
   <-1, 1, 0>, <-0.5, 2, 0>, <0.5, 2, 0>, <1, 1, 0>, <-1, -1, 0>, <1, -1, 0>
})
PlotXYSpline(splSeconda)

Due curve nella stessa immagine

Altre funzioni per generare spline sono:

splCubicSpline(array[n] {v1, v2, ..., vn})

è analoga alla curva generata quando si usa l'istruzione cubic all'interno di un prisma: la curva partirà da v2 e terminerà in vn-1, mentre i punti v1 e vn danno l'andamento alle estremità. Le differenze sono le stesse della curva lineare: la curva può anche essere aperta e può anche non giacere tutta nello stesso piano, ma essere "sghemba". Per esempio

#declare splCurvaCubica = splCubicSpline(array[6] {
   < 3, -5, 0>, //nodo di controllo
   < 3,  5, 0>, <-5,  0, 0>, < 3, -5, 0>, < 3,  5, 0>, 
   <-5,  0, 0> //nodo di controllo 
})
PlotXYSpline(splCurvaCubica)

Esempio di curva cubica in un piano

splBezierSpline(array[n] {v1, v2, ..., vn})

genera una curva cubica definita attraverso il poligono di Bezier. Ogni quattro punti verrà generato un tratto di curva che passa per il primo e il quarto punto e ha il secondo e il terzo come punti di controllo. Stesse analogie e differenze rispetto ad un prisma definito con l'opzione bezier.

Come avete visto finora, ho usato il prefisso "spl" per tutte quelle funzioni che restituiscono una spline. Sarà vero per tante altre funzioni di cui vi parlerò e seguirò regole analoghe per altri particolari oggetti.

Ora avete già una grande capacità espressiva, credetemi. Sapete quasi tutto...


Le superfici che potete definire hanno due parametri obbligatori, ed entrambi sono curve spline.

Il primo dei parametri è la forma che volete tenere in mano. La forma (shape, nel programma) è una qualunque curva, planare o sghemba. Ma c'è un sistema di riferimento privilegiato.

Se definite come forma una curva che giace completamente nel piano z=0, essa, camminando lungo il percorso, rimarrà sempre perpendicolare ad esso. Pensate ad un tubo di gomma, di quelli per innaffiare: potete piegarlo imponendogli qualunque curva, ma continua sempre ad avere una sezione rotonda.

Voi potete definire la vostra forma ovunque vogliate nello spazio, ma gli allineamenti verranno effettuati rispetto ad un preciso piano di riferimento: sarà il piano xy del sistema in cui viene disegnata la curva a rimanere sempre perpendicolare al cammino. Se disegnate una forma sghemba, la condizione di perpendicolarità potrebbe non essere rispettata ovunque.

Per creare una curva che rappresenti un cerchio giacente nel piano xy, con centro nell'origine e di raggio 1, potete usare l'istruzione:

#declare MyShape = splCircle(4)

vi spiegherò più avanti il suo utilizzo, ma sappiate che genera la spline:

Una circonferenza di raggio 1

Ora stabiliamo che questa è la forma che deve avere la mia superficie: lo faccio con l'istruzione

SetShapeSpline(MyShape)

L'altra metà del lavoro è nello scegliere il cammino (path, nel programma) che verrà seguito dalla forma.

Per descrivere il cammino del tubo basta definire una spline, quindi lanciare la macro SetPathSpline(). Il percorso può essere una qualsiasi curva, planare o sghemba. La forma impostata con la macro SetShapeSpline() la seguirà fedelmente, mantenendosi sempre perpendicolare. Volete per esempio avanzare in linea retta? Fatelo con le istruzioni

#declare MyPath = splLinearSpline(array[2] {<0, 2, 0>, <8, 2, 0>})
SetPathSpline(MyPath)

Ora occorre generare l'oggetto vero e proprio: lo faccio con queste istruzioni:

object {
   Striscia(UNION)
   texture { pigment { color rgb <1, 1, 0> } }
}

Abbozzo un ambiente...

camera { location <-2, 6, -4> look_at <3, 1, 0> }
light_source { <-50, 50, -0> color rgb 1}
plane { y, 0 pigment { checker color rgb 1 color blue 1 } }

Lancio la scena e...

Ok, è solo un cilindro, ma...

Volete che il vostro tubo proceda a zigzag? Fatelo cambiando solo il percorso, con

#declare MyPath = splCubicSpline(array[8] {
        <-1, 0, 0> <0, 0, 0>, <1, 0, 1>, <2, 0, -1>, 
        <3, 0, 1>, <4, 0, -1>, <5, 0, 0>, <6, 0, 0>})
SetPathSpline(MyPath)

...questo è già qualcosa di nuovo

L'argomento tra le parentesi della macro Striscia() indica che tipo di oggetto generare. Può assumere uno di questi valori:

Se definite un percorso che abbia degli spigoli, anche la superficie avrà uno spigolo. Non posso fare come fanno i corniciai, che tagliano i listelli di legno a 45° in modo da ottenere i miei spigoli: non è una soluzione adatta a tutte le forme. Dunque uso un'altra tecnica: quella di stirare le sezioni avvicinandomi allo spigolo, in modo che sia da un lato che dall'altro esse si avvicinino ad un piano in comune.

Tutto viene effettuato automaticamente. C'è solo da specificare, eventualmente, quanto dev'essere lunga l'area in cui le sezioni vengono deformate. Potete farlo impostando la variabile gCorrArea. Il valore di default è gCorrArea=2: la deformazione ha effetto in un'area che si estende per 2 unità prima e dopo lo spigolo (camminando sul percorso). In quest'area non c'è perpendicolarità tra le sezioni e il percorso. Il valore di default è adatto a sezioni di dimensioni all'incirca unitarie: se la sezione fosse molto più grande si potrebbero creare ammaccature e deformazioni eccessive (la superficie che collassa su se stessa...). In questo caso dovreste aumentare il valore di gCorrArea.

La curva reagisce bene anche con un angolo nel percorso di 180°: in questo caso la curva non viene deformata (dovrebbe essere deformata infinitamente), ma è come se "rimbalzasse" su un piano. Ci sarà un enorme buco, ma più avanti vi mostrerò come coprirlo.

Ecco due disegni che mostrano come lavora la routine di distorsione degli angoli. È il dettaglio di una striscia con sezione circolare di raggio 1/5 e percorso quadrato di lato 1. La striscia è tagliata a metà per mostrarne l'interno. I quadretti sono di lato 1/10.

gCorrArea=1/3
l'area di correzione è abbastanza grande rispetto alla sezione.
gCorrArea=1/25
l'area di correzione è troppo piccola, la curva prima "entra" nello spigolo, quindi si ripiega su se stessa per allinearsi.

Attenzione: lo spigolo potrebbe non essere visualizzato correttamente se la sezione è una curva sghemba. In questo caso potrebbero esserci buchi sulla superficie. Qualcuno ha idee in proposito?


Mi presento, io sono quella paziente persona che ha parlato alla macchina col linguaggio dei triangoli, e che vi ha disegnato tutti quei puntini sul monitor. Vi giuro che per tutto il lavoro che ho svolto, sono stato fin troppo silenzioso. Come mi chiamo non importa, non credo che ve lo dirò. Ma sento il bisogno di raccontarvi il modo in cui lavoro, perché possiate trarne beneficio...

Voi mi avete consegnato la vostra forma. Io cammino lungo il percorso, e per ogni punto disegno la forma con queste regole:

  1. posiziono la forma in modo che l'origine del sistema in cui l'avete disegnata coincida con il punto sul percorso (ma ora ho bisogno di allinearla);
  2. oriento l'asse z del sistema di riferimento con la direzione del percorso (ora in una certa direzione l'ho allineata, ma è ancora libera di ruotare su se stessa);
  3. ruoto la curva in modo che l'asse y del sistema di riferimento si spinga in direzione del cielo.

A questo punto vi rendete conto che potrei avere un problema: se a un certo punto il vostro cammino sale dritto verso l'alto (cioè in direzione y), una volta orientata la curva, non so come ruotarla. È lo stesso problema che avreste se definite una "camera" che guardi dritta verso l'alto o verso il basso: non si sa come ruotare l'operatore in modo da fargli tenere la testa in alto. Per ovviare a questo problema dovete suggerirmi un nuovo cielo. Potete farlo impostando la variabile gvSky, per esempio con l'istruzione

#declare gvSky = z;

Ricordatevi delle mie parole: orribili saranno i risultati di chi non rispetta questa semplice regola... Ma... ormai è tempo che torni al lavoro. Buonasera.


Già da ora si vede che diventa importante avere la massima espressività nel descrivere le curve di controllo. Usando le cubiche "alla Bezier" avete tutta la potenza che volete, ma per alcuni compiti facili, soprattutto per la definizione di alcune semplici forme, sono più complesso di quanto desiderabile. Ci sono alcune macro molto più facili da usare che restituiscono spline.

Una è già stata vista: la macro splCircle(n).

splCircle(n)

genera un cerchio contenuto nel piano z=0 di raggio 1. Il parametro n indica in quanti tratti dev'essere diviso il cerchio: infatti è impossibile descrivere un cerchio intero usando solo un tratto di cubica. Per ottenere buoni risultati ci vuole un n maggiore o uguale a 4. Oltre 4 il risultato non migliora; il motivo per cui occorrerebbe scegliere un numero maggiore è un altro. Più avanti scopriremo infatti che si può trasformare una curva in un'altra. Ma il vincolo che devono rispettare le due curve è quello di essere composte dallo stesso numero di tratti. Posso trasformare un quadrato in un cerchio diviso in 4 parti oppure un esagono in un cerchio diviso in 6 parti.

splArc(V)

genera un arco sul cerchio nel piano z=0 e raggio 1. V è un array in cui ogni elemento indica, in gradi, la posizione di un punto di separazione nell'arco. Dunque un array di n componenti genererà un arco composta di n-1 tratti curvilinei. L'angolo 0 coincide con l'asse x e il verso degli angoli è quello indicato dalle dita della mano sinistra, se il pollice punta in direzione +z. Ogni tratto non dovrebbe estendersi per più di 90 gradi, altrimenti si ammaccherà. Per esempio:

  • splArc(array[5] {0, 90, 180, 270, 360}) è la stessa circonferenza definita da splCircle(4);
  • splArc(array[4] {0, 30, 60, 90}) genera un arco di 90 gradi, dal punto <1, 0, 0> al punto <0, 1, 0>, diviso in 3 parti.
  • splArc(array[9] {0, 90, 180, 270, 360, 450, 540, 630, 720}) strano: fa 2 giri. A che servirà mai? Per esempio, in cooperazione con...

splSumSplines(spl1, spl2)

genera una spline sommandone due. Per esempio:

#declare splPrima = splArc(array[9] {
   0, 90, 180, 270, 360, 450, 540, 630, 720
})
#declare splSeconda = splLinearSpline(array[9] {
   <0, 0, 0>,   <0, 0, 1/3>, <0, 0, 2/3>, 
   <0, 0, 3/3>, <0, 0, 4/3>, <0, 0, 5/3>, 
   <0, 0, 6/3>, <0, 0, 7/3>, <0, 0, 8/3>
})
#declare splSomma = splSumSplines(splPrima, splSeconda)
PlotSpline(splSomma, <3, 1, 3>)

Una spirale

splPolygon(n)

genera un poligono regolare con n lati, giacente nel piano z=0 e inscritto nella solita circonferenza unitaria. Il primo vertice coincide sempre con il punto <1, 0, 0>. Per esempio un ottagono sarà dato da:

#declare splOttagono = splPolygon(8)

splTranslate(Spline, V)

splRotate(Spline, V)

splScale(Spline, V)

effettuano sulle spline le stesse trasformazioni delle omonime trasformazioni per le primitive translate, rotate, scale. In più, è possibile scalare una spline per un vettore che abbia alcune componenti uguali a 0, al massimo rischiate di creare qualche triangolo degenere.

Un'importante differenza è che queste macro vanno usate come funzioni, dunque restituiscono un risultato che va assegnato a qualche variabile, che può essere anche la stessa spline di partenza. Per esempio:

// Crea un rombo
#declare splSpline = splPolygon(4)
// Disegna il rombo in rosso
PlotXYSpline(splSpline)

// Trasforma il rombo in quadrato
#declare splSpline = splRotateSpline(splSpline, z * 45)
// Trasforma il quadrato in rettangolo
#declare splSpline = splScaleSpline(splSpline, <2, 0.5, 1>)
// Sposta il rettangolo
#declare splSpline = splTranslateSpline(splSpline, <1, 1, 0>)
// Disegna il rettangolo in verde
PlotXYSpline(splSpline)

La stessa curva, prima e dopo alcune trasformazioni

splCombineSplines(Spl1, Spl2)

Unisce due spline. Questo non vuol dire che otterrete una unica spline continua: se le due spline non si toccano, continueranno a non toccarsi anche dopo essere state unite.

splJoinSplines(Spl1, Spl2)

Unisce due spline. Trasla la seconda spline in modo che il suo punto iniziale coincida col punto finale della prima, quindi le salda e ne restituisce il risultato. Questo dunque sarà sempre una spline continua.


Bello vero? avete fatto un tubo. Brutto vero? è spigoloso, bruttino...

Ovviamente non voglio lasciarvi quell'orrore triangoloso sui monitor: è possibile migliorare la precisione con cui la superficie viene disegnata.

Se l'approssimazione dei triangoli è troppo scarsa, potete scegliere di far dividere le curve in un numero maggiore di parti. Si fa impostando alcune variabili globali.

Sono le due variabili principali: ogni tratto della spline con cui descrivete il percorso viene diviso in un numero di segmenti pari a gPathDiv; allo stesso modo, ogni tratto della spline con cui avete descritto la forma, viene diviso in gShapeDiv segmenti. Ogni volta che raddoppiate uno di questi numeri, raddoppia il numero di triangoli generato: raddoppiandoli entrambi, il loro numero quadruplica...

I valori di default sono gPathDiv=4 e gShapeDiv=4.

Aumentare a dismisura le divisioni può non essere furbo: nel secondo esempio, il tubo a zig-zag, la curva è composta da parti molto curve e parti quasi dritte. Aumentare a dismisura la variabile gPathDiv vuol dire aumentare le suddivisioni anche nelle parti dritte, che si traduce in maggior spreco di tempo e memoria contro nessun aumento di qualità.

A questo inconveniente si rimedia usando un algoritmo adattivo: si abilita impostando un valore maggiore di zero alla variabile gPathDepth. In questo caso, se un tratto del percorso è particolarmente curvo, esso verrà spezzato in due parti, e ogni metà verrà ulteriormente suddivisa. Questo processo di suddivisione può terminare in due casi:

  1. quando ho suddiviso un segmento per gPathDepth volte;
  2. quando stimo che sto per compiere un errore di visualizzazione minore di gPathErr.

Il numero di segmenti utilizzati per approssimare la curva non è più costante: aumenta dove la curva è più contorta. Per ogni segmento di percorso userò da un minimo di gPathDiv a un massimo di gPathDiv*(2^gPathDepth) segmenti.

Considerazioni del tutto analoghe valgono per il controllo dell'errore e la suddivisione successiva delle sezioni.

Se viene raggiunta la profondità di suddivisione massima, ma c'è ancora un errore maggiore di quanto richiesto, verrete avvertiti alla fine del parsing della superficie. A quel punto potete scegliere se aumentare la profondità di suddivisione (a discapito di tempo di elaborazione e memoria occupata) o accontentarvi del risultato...

I valori di default sono gPathDepth = gShapeDepth = 0 e gPathErr = gShapeErr = 0.01.

I valori dell'errore non sono "alti" o "bassi" in assoluto: dipende dalla scala in cui costruite il vostro oggetto, dalle dimensioni dell'oggetto nell'immagine e dalla dimensione dell'immagine stessa. Quelli di default sono adatti ad oggetti di dimensioni grossomodo unitarie, ma a volte si può ancora ottenere un cerro margine di miglioramento.

Tornando all'esempio precedente, se aggiungete alla scena (prima del comando Striscia()) le righe

#declare gPathDepth = 3;
#declare gShapeDepth = 3;

otterrete:

Un risultato migliore del precedente

Le variabili gPathDiv, gPathDepth e gPathErr lavorano anche quando lanciate le macro PlotSpline e PlotXYSpline, modificando la qualità dei grafici.

La routine che si incarica di valutare l'errore lungo il percorso compie in realtà un'approssimazione: è infatti ottimizzata per valutare l'errore come se ad essere disegnata fosse una sezione quadrata di lato 2 centrata sul percorso. Per superfici con sezioni molto più grandi potrebbe risultare alla fine un errore maggiore di quello richiesto.

Inoltre, l'errore lungo il percorso viene pesato solo in un vertice del quadrato, precisamente quello che ha entrambe le coordinate positive nel piano della sezione. Questo potrebbe portare ad asimmetrie nella valutazione dell'errore.

Considerate infine che in alcuni (piuttosto rari) casi, la routine di valutazione dell'errore compie... errori, ma veramente grossolani. Se, ad esempio, ho una curva a forma di S, perfettamente simmetrica, e la spezzo a metà, nel misurare l'errore sembrerà che esso sia nullo, poiché la routine misura di quanto il punto intermedio della curva si discosta da quello della retta congiungente gli estremi. In questo caso l'"errore" sembra 0, di conseguenza la S viene approssimato con un unico segmento. Per evitare problemi del genere (potreste accorgervene perché aumentando la profondità di divisione non aumenta la qualità) provate ad aumentare o diminuire di 1 le divisioni iniziali: questo servirà a spezzare insidiose simmetrie.


Ormai siete grandi, è tempo ormai che sappiate tutto quello che c'è da sapere sulla deformazione della curva mentre viene spostata. Ma prima dovrete imparare un altro tipo di controllo, anche questo già incontrato nel linguaggio POV.

Il controllo che vi presento è la funzione cubica definita a tratti, che chiamo slope. È la stessa funzione che si imposta quando si modifica un pattern normal con il comando slope_map. Una funzione del genere, come una curva, posso vederla come oggetto e darle un nome. La genero usando la funzione:

slpSlope(array[n] {<X1, Y1, M1>, <X2, Y2, M2>,..., <Xn, Yn, Mn>})

Il parametro è un array di vettori 3D, ma le componenti di questi vettori non vanno lette come componenti x, y e z di un punto nello spazio, ma rispettivamente come

Se voglio creare una funzione definita nell'intervallo da 0 a 3, che parta dall'origine con direzione orizzontale, salga fino a toccare il valore 1 quando x è uguale a 2 e poi scenda fino a 0 concludendo in discesa, posso fare

#declare slpMySlope = slpSlope(array[3] {
   <0, 0, 0>, <2, 1, 0>, <3, 0, -2>
})

Volete vedere quale sia il grafico della funzione che avete ottenuto? Basta aggiungere alla scena la macro PlotSlope(slpMySlope). Con la precedente definizione di slope otterrete il grafico:

Una semplice slope

Anche la qualità di questo grafico è pilotata dalle solite variabili gPathDiv, gPathDepth e gPathErr.

Un esempio più complesso di slope, copiato e leggermente modificato dal manuale del POV, è:

/*#declare MySlope = slpSlope(array[15] {
   < 0, 0, 1>,   // Do tiny triangle here
   < 2, 2, 1>,   //  down
   < 2, 2,-1>,   //     to
   < 4, 0,-1>,   //       here.
   < 4, 0, 0>,   // Flat area
   < 5, 0, 0>,   //   through here.
   < 5, 2, 0>,   // Square wave leading edge
   < 6, 2, 0>,   //   trailing edge
   < 6, 0, 0>,   // Flat again
   < 7, 0, 0>,   //   through here.
   < 7, 0, 3>,   // Start scallop
   < 8, 2, 0>,   //   flat on top
   < 9, 0,-3>,   //     finish here.
   < 9, 0, 0>,   // Flat remaining through 1.0
   <10, 0, 0>
})

Una slope più complessa

Se volete creare una slope composta di soli tratti rettilinei, potete usare la macro slpLinearSlope(V) dove V è un array di vettori 2D. Le componenti di questi vettori vanno lette rispettivamente come:

Bella, vero? Peccato che non funzioni!! :(

Il problema è un bug del POV (non ancora corretto fino alla versione 3.1a), che impedisce di leggere correttamente le componenti di un vettore 2D. La vostra versione è difettosa? In questo caso usate la macro slpLinearSlopeBug(V), dove V è un array di vettori 3D. Le prime due componenti funzioneranno come per la versione slpLinearSlope(V), mentre la terza verrà ignorata...

Ovviamente io non ho mai testato la slpLinearSlope(), per cui non mi ci giocherei la casa che funzioni...


Si era detto che la curva sarebbe stata spostata e insieme deformata, ma finora non si è ancora deformato niente. Dunque facciamo qualcosa di interessante.

Io disegno una curva, poi la applico al percorso. Posso pensare di traslare la curva e poi applicarla al percorso, o ridimensionarla o ruotarla. Ma perché non fare queste cose dinamicamente, cioè variando l'entità della trasformazione lungo il cammino? Ci si possono fare tante cose...

Se il percorso è curvo, è difficile sapere quanto sia lungo. Ma potreste aver bisogno di quest'informazione. Una volta impostato il percorso con la SetPathSpline(), potrete saperlo leggendo la variabile gPathLength.

Attenzione!!! Non cambiate il valore della gPathLength, ma limitatevi a leggerlo. O saranno problemi...

Supponiamo di creare una specie di toro, con raggio maggiore 3 e raggio minore 1, ma a sezione quadrata. Lo faccio con:

#declare splPath = splCircle(4)                 // Cerchio nel piano xy
#declare splPath = splRotate(splPath, x * 90)   // Porto il cerchio in xz
#declare splPath = splScale(splPath, <3, 3, 3>) // Porto il raggio a 3
SetPathSpline(splPath)                          // Lo applico come percorso

#declare splShape = splPolygon(4)               // Quadrato nel piano xy
SetShapeSpline(splShape)                        // Lo applico come forma

Ma voglio che il quadrato, girando intorno al cerchio del raggio maggiore, compia una rotazione completa su se stesso. Definisco una slope che mi indichi la rotazione:

#declare slpRotSlope = slpLinearSlope(array[2] {<0, 0>, <gPathLength, 360>})
AddShapeTransform(PATH_ROTATE, slpRotSlope)

Un toro, diverso dal solito

La AddShapeTransform() prende due parametri: il primo indica il tipo di trasformazione da effettuare. Può essere uno dei valori:

Il secondo valore passato alla funzione è una slope che indica l'entità della trasformazione, e che dovrebbe essere lunga quanto la lunghezza del percorso. In ogni punto del percorso, la slope indica con precisione l'entità della trasformazione della forma corrispondente. L'ascissa delle slope è la cosiddetta ascissa curvilinea, e corrisponde alla distanza percorsa partendo dall'inizio del percorso e camminando su di esso (più precisamente è la sua classica approssimazione: la parametrizzazione in lunghezza di corda).

Un altro esempio: un toro che abbia il raggio minore variabile. Il percorso è lo stesso di prima:

#declare splShape = splCircle(4)
SetShapeSpline(splShape)

#declare slpScaleSlope = slpSlope(array[3] 
   {<0, 1.2, 0>, <gPathLength/2, 0.5, 0>, <gPathLength, 1.2, 0>
})
AddShapeTransform(UNIF_SCALE, slpScaleSlope)

Un'altra variazione sul tema

Si può specificare qualunque numero di trasformazioni vogliate: esse verranno applicate nell'ordine in cui vengono definite. Valgono tutte le considerazioni valide per le trasformazioni delle primitive: se ad esempio prima traslo una forma e poi la faccio ruotare, ottengo anche un'orbita (attorno alla direzione del percorso). Se prima la ruoto e poi la traslo non ci saranno orbite, ma solo una rotazione sul posto.

Ad esempio, posso applicare la stessa trasformazione nella dimensione della sezione del secondo esempio alla superficie rotante del primo esempio:

Fusione dei due esempi precedenti

Ecco un esempio di composizione di trasformazioni: quest'oggetto lo chiamo "conastro". L'ha tirato fuori dal suo cappello magico il prof. Antonio Corbo Esposito, il mio insegnante di Analisi Matematica, per una prova scritta di Analisi II. Chiedeva di calcolarne superficie e volume e di stabilire se si regge in piedi o casca sul fianco. In piedi ci si regge, ma io non l'ho passato, quel compito... È come un cono di raggio di base 1 e altezza 1, ma, salendo, si torce attorno ad un asse verticale che tocca il bordo della base.

// Il percorso e' un tratto verticale da <0, 0, 0> a <0, 1, 0>
#declare gvSky = z;
#declare splPath = splLinearSpline(array[2] {<0, 0, 0>, <0, 1, 0>}) 
SetPathSpline(splPath)                      

// La forma e' una circonferenza di raggio 1
#declare splShape = splCircle(4)
SetShapeSpline(Shape)

// La forma, avanzando, diventa _quasi_ di raggio 0
#declare slpScale = slpLinearSlope(array[2] {<0, 1>, <1, 0.0001>})
AddShapeTransform(UNIF_SCALE, slpScale)

// Traslo la forma, in modo che, ruotando, orbiti
#declare slpTrans = slpLinearSlope(array[2] {<0, 1>, <1, 1>})
AddShapeTransform(HORZ_TRANSLATE, slpTrans)

// La forma, avanzando, ruota attorno al percorso
#declare slpRotate = slpLinearSlope(array[2] {<0, 0>, <1, 360>})
AddShapeTransform(PATH_ROTATE, slpRotate)

Il "conastro"

La punta preferisco concluderla con un valore molto piccolo piuttosto che con 0. In questo caso avrei avuto la generazione di alcuni triangoli degeneri, ma nient'altro di grave.

Probabilmente è stato per tentare di disegnare quest'oggetto col POV che ho cominciato a pensare a questa utility.

Quando applicate qualunque trasformazione alle sezioni, esse verranno prese in considerazione nel valutare gli errori di campionamento: sia (ovviamente) in direzione delle sezioni ma anche in direzione del percorso. È come se le trasformazioni venissero applicate al fantomatico quadrato perso a modello per misurare l'errore. Dunque per avere una buona precisione di campionamento per sezioni molto grandi (o una non eccessiva suddivisione per sezioni molto piccole) potreste disegnare le sezioni con dimensioni grossomodo unitarie per poi ingrandirle o rimpicciolirle con una trasformazione.


Un altro modo di deformare la curva mentre si sposta è quello di impostare diverse sezioni in diversi punti del percorso. Nei punti intermedi la forma sarà una miscela della precedente e della successiva.

Come esempio, ecco una primitiva semplice da pensare ma non ottenibile facilmente col linguaggio POV: un cilindro che si trasforma in box.

Per prima cosa imposto il percorso. Mi va bene anche dritto...

#declare Path = splLinearSpline(array[2] {<-1, 1, 0>, <2, 1, 0>})
SetPathSpline(Path)

Per definire la forma, non va più usata la macro SetShapeSpline: c'è una versione più versatile:

AddShapeSpline(splSpline, X)

Questa macro dice che la sezione, nel punto X, avrà la forma splSpline. Il punto X è indicato con la solita ascissa curvilinea, dunque dovrebbe essere compreso tra 0 e gPathLength. Nell'esempio del cilindro che si fa scatola...

#declare Shape = splCircle(4)
#declare Shape = splRotateSpline(Shape, z * 45)
AddShapeSpline(Shape, 0)

#declare Shape = splPolygon(4)
#declare Shape = splRotateSpline(Shape, z * 45)
#declare Shape = splScaleSpline(Shape, <1.4, 1.4, 0>)
AddShapeSpline(Shape, 3)

Un cilindro che diventa scatola

Ora la superficie non sarà più definita dall'inizio alla fine del percorso, ma solo dalla prima all'ultima delle forme (non importa in quale ordine vengano impostate). Ritengo superfluo dirvi che ci vogliono almeno due forme in due punti diversi per vedere qualcosa...

Una superficie può essere disegnata solo se ogni sezione impostata è composta dallo stesso numero di tratti.


Ogni sezione di superficie è una media pesata della forma precedente e di quella successiva al punto di cui sto generando la sezione. Le eventuali forme più lontane non entrano in gioco. Quale percentuale dell'una o dell'altra forma vengano utilizzate, dipende dalla distanza della sezione dalle forme: se sto morphando un quadrato in un cerchio, quando sono più vicino al quadrato costruisco forme più simili al quadrato che al cerchio.

C'è un controllo che mi consente di stabilire manualmente le percentuali delle forme usate per comporre le sezioni lungo il percorso. Passando da un quadrato ad un cerchio, potrei stabilire ad esempio che la superficie resti un cerchio per la maggior parte del percorso, poi improvvisamente diventi un quadrato.

Per pilotare le quantità delle shapes definite dall'utente che entrano in gioco nella costruzione di una sezione di superficie, si usa una slope. Questa slope dovrebbe restare compresa tra i valori 0 e 1, ed indica, per ogni punto, la percentuale della forma successiva utilizzata per costruire la sezione in quel punto. La percentuale della forma precedente sarà 1 meno la percentuale della successiva. Dunque un valore 0 vuol dire che si usa solo la forma precedente, 1 solo quella successiva, 0.3 vuol dire che si usa il 70% della sezione precedente e il 30% di quella successiva.

È anche possibile utilizzare valori minori di 0 o maggiori di 1. Ma i risultati sono decisamente poco intuitivi.

Ecco una serie di esempi di grafici di funzioni di morph con il relativo effetto sul "cilindro in box" dell'esempio precedente:

Il normale morph, senza modificatori

Cerchio per 2/3, poi velocemente quadrato.

Cerchio per 1/3, poi 1/3 di morph, poi 1/3 di quadrato

Effetto ping-pong

Valori esterni all'intervallo tra 0 e 1: effetto interessante, ma poco intuitivo...


Bella la texture del conastro, vero? Come l'ho fatta? La tecnica si chiama uv mapping, e consiste nel mappare su di una superficie (contorta quanto volete) una funzione (in questo caso la texture) definita su un'area di forma più semplice.

Immaginate l'oggetto

polygon { <0, 0, 0>, <1, 0, 0>, <1, 1, 0>, <0, 1, 0>, <0, 0, 0> }

che rappresenta un quadrato di lato 1 nel piano z=0. Immaginate di applicargli una qualunque texture. La stessa porzione di texture che vedreste su una delle facce del quadrato verrà presa e deformata fino ad aderire ai bordi della superficie.

Per farlo, si usa la macro

SetWrappedTexture(Texture)

con qualunque texture passata per parametro, anche a più strati, comprendente pigment, finish e normal.

Nell'esempio del conastro ho usato le istruzioni:

SetWrappedTexture(texture {
   pigment {
      checker color rgb 1 color blue 1
      scale 1/16
   }
   finish {
      specular 0.8		
   }
})

Il lato inferiore del quadrato verrà mappato sulla forma all'inizio del percorso, mentre il lato superiore corrisponderà alla fine di esso. Il lato sinistro (quello caratterizzato dall'avere coordinata x=0) corrisponderà all'inizio delle sezioni, mentre il lato destro alla fine di esse.

Meno parole e più disegni... Il comando per adattare il bitmap e' stato:

SetWrappedTexture(texture {
   pigment {
      image_map {
         png "c:\\Disegni\\Me.png"
         interpolate 2
         once
      }
   }
})


Questo sono io in un angolo...

Questo sono io sotto i riflettori!

La freccia blu indica il percorso; le frecce rosse indicano due sezioni.

Attenzione: Non potete generare una MESH se usate una texture mappata.


La generazione di una superficie può essere un lavoro abbastanza lento. Se devo comporre la mia scena, non vorrò perdere tutto il tempo necessario alla generazione ogni volta che devo spostare un soprammobile di un centimetro. Anche se voglio fare un'animazione, non vorrò perdere tempo a generare sempre la stessa superficie ad ogni fotogramma. Posso chiedere che i dati generati dalla routine vengano salvati in un file.

Per memorizzare i dati di una superficie in un file, basta dare un nome al file da creare, utilizzando la variabile gsBufferName. Per esempio

#declare gsBufferName = "Conastro"

fa sì che tutto quello che viene generato dalla routine venga salvato nel file "Conastro.inc". Non va data l'estensione al nome del buffer. Il file viene creato nella stessa directory della scena, ma può anche essere esplicitamente specificato un percorso.

La seconda volta che lancerete la scena, la routine si accorgerà della presenza del file salvato (sempre che sia in una posizione raggiungibile, come nella stessa directory della scena o in una delle directory indicate dalle opzioni LIBRARY_PATH) e lo leggerà, senza generare nuovamente i triangoli.

Se volete cambiare qualcosa dell'oggetto, dovrete modificare i parametri ed eliminare il file di buffer prima di lanciare nuovamente la scena.

Se salvate in un buffer una superficie con una texture avvolta attorno, nel file ci sarà solo l'indicazione della trasformazione e non la definizione completa della texture: in questo modo potrete modificare la texture senza dover generare una nuova superficie ogni volta.

Attenzione! Se interrompete la macro mentre sta disegnando la curva, un file verrà generato ugualmente, ma non sarà corretto. Al lancio successivo otterrete strani errori (tipo "missing }"). In questo caso cancellate il vostro file di buffer e lanciate nuovamente il parsing. Per aggirare questo problema avrei bisogno di istruzioni per rinominare o cancellare file che il POV non ha (ancora).

La routine legge anche un file di buffer compresso utilizzando il compressed mesh macro file di Chris Colefax. Basta che il file con estensione .pcm abbia lo stesso nome del file .ini da cui viene generato. Questa compressione non supporta però le superfici con le texture adattate. Devi avere il file pcm.mcr in un percorso di libreria: lo puoi trovare nella homepage di Chris Colefax. Forse in futuro farò l'output direttamente in questo formato, ma lo conosco ancora poco...


Vogliamo chiuderlo questo tubo, che entra freddo?

Potete aggiungere i tappi alla vostra superficie usando i comandi

object { Diaframma(          0, POLYGON) texture {MiaTexture}}
object { Diaframma(gPathLength, POLYGON) texture {MiaTexture}}

Il primo parametro è la posizione della chiusura. Normalmente vorrete chiudere la superficie all'inizio e alla fine, ma non è un obbligo. Potreste voler aggiungere dei diaframmi internamente e guardarli attraverso buchi nella superficie, o addirittura non disegnare per niente la superficie, ma solo i diaframmi... la posizione al solito è espressa come ascissa curvilinea.

Il secondo parametro indica che tipo di oggetto generare: può essere

Potete usare un diaframma anche se volete coprire il buco creato da un angolo a 180° nel percorso: basta metterlo esattamente nel punto dell'angolo.

Per esempio, il codice:

#declare splPath = splBezierSpline(array[8] {
   <0, 0, 0>, <-6, 6, 0>, <0, 9, 0>, <0, 5, 0>
   <0, 5, 0>, <0, 9, 0>, <6, 6, 0>, <0, 0, 0>
})
SetPathSpline(splPath)
#declare gvSky = z;

#declare splShape = splCircle(4)
#declare splShape = splScaleSpline(splShape, <1,1,1>*0.5)
SetShapeSpline(splShape)

union {
   object { Striscia(UNION) }
   object { Diaframma(gPathLength/2, POLYGON) }
   pigment { color red 1 } finish { phong 0.7 }
}

genera:

Cuore e diaframma...

Prima vi dico cos'è un insieme stellato: è un insieme in cui c'è un punto che può essere collegato tramite un segmento di retta a tutti gli altri punti dell'insieme. Proprio come nel disegno di una stella, dal cui centro posso toccare tutti i bordi muovendomi in linea retta. Poiché tutti i triangoli che uso per chiudere la superficie devono avere un vertice in comune, è necessario che la forma delimiti un insieme stellato, per poter essere chiusa con questo metodo.

Per default, i triangoli convergono sul punto in cui il piano di riferimento della forma tocca il percorso. Ma è possibile stabilire qualunque punto dove convergere impostando la variabile gvShapeCenter.

Questa variabile indica un punto nel sistema di riferimento delle curve delle forme. Ci penserà la routine a sistemarlo nello spazio. Per esempio per la curva

#declare splSpline = splCircle(4)
#declare splSpline = splTranslate(splSpline, <4, 0, 0>)

sarebbe utile impostare

#declare gvShapeCenter = <4, 0, 0>;

I poligoni non soffrono di questo problema. Hanno invece il problema che vanno necessariamente definiti su un unico piano. Dunque se avete una curva sghemba e così contorta da non descrivere un insieme stellato, potreste non riuscire ad ottenere ciò che volete.

Usando i triangoli potete anche creare dei tappi di forma conica. Basta non mettere il gvShapeCenter sul piano z=0. Per esempio:

#declare gvShapeCenter = <0, 0, 0.75>;
object {Diaframma(gPathLength/2, UNION) }

Il diaframma ora è di forma conica.


Se volete includere più strisce nella stessa scena, potete resettare i parametri di controllo usando la macro ResetParameters(). Verranno resettati il percorso e le forme, le slope di controllo, la texture rimappata e il nome del buffer. Rimarranno inalterate altre variabili, quali la direzione del cielo e i parametri di suddivisione della superficie in triangoli.


Per appoggiare un qualunque oggetto sulla superficie potete usare la trasformazione PutOnSurface(X,T). Le coordinate X e T indicano il punto sulla superficie: X è la coordinata in direzione del percorso e può andare da 0 a gPathLength; T è la coordinata in direzione della sezione ed è sempre compresa tra 0 e 1, qualunque sia la reale dimensione della sezione.

L'oggetto verrà preso e traslato sul punto corrispondente della superficie. L'asse x del sistema di riferimento dell'oggetto sarà reso parallelo alla sezione mentre l'asse y sarà perpendicolare alla superficie. L'asse z sarà ortogonale agli altri due (non necessariamente sarà parallelo al percorso).

La PutOnSurface() funzione bene se la sezione è una curva che gira in senso antiorario, come quelle generate dalla splCircle() e simili. Se avete una sezione che gira in senso orario, l'oggetto finirebbe dalla parte sbagliata della superficie. In questo caso potete utilizzare la PutUnderSurface(), che si usa esattamente allo stesso modo, ma mette l'oggetto dalla parte opposta.

Per esempio, riciclando il "cilindro in box" posso aggiungere:

// ciclo lungo il percorso
#declare I=0;
#while (I<=8)
   
   // ciclo lungo le sezioni
   #declare J=0; 
   #while (J<16)
      
      // Un cono piantato a terra
      cone {
         <0, -0.05, 0>, 0, <0, 0.2, 0>, 0.05
         pigment {color blue 1}
         
         // Trasportato sulla superficie
         PutOnSurface(I/8*gPathLength, J/16)
      }
      #declare J=J+1;
   #end
   #declare I=I+1;
#end

Spilli piantati sulla superficie

Per un'interazione ancora più profonda della scena con la superficie, potete leggere singolarmente i valori della matrice di trasformazione restituita da PutOnSurface(). La funzione bBaseOnSurface(X,T) restituisce un array di 4 vettori che rappresentano rispettivamente gli assi x, y e z e la traslazione del sistema di riferimento poggiato sulla superficie nel punto richiesto. Potete dunque usare questi valori come volete.

Per esempio, per simulare la superficie in wire-frame posso collegare una griglia di punti sulla superficie con sottili cilindri.

// Texture per i cilindri
#declare txtWireFrame = texture {
   pigment {color rgbf <0, 1, 0, 0.8>}
   finish {ambient 0.6 diffuse 0.4}
}

// Buffer per conservare i punti
#declare Points = array[9][16]

// Campionamento dei punti
#declare I=0;
#while (I<=8)
   #declare J=0;
   #while (J<16)
      #declare bBase = bBaseOnSurface(I/8*gPathLength, J/16)
      #declare Points[I][J] = bBase[3];
      #declare J=J+1;
   #end
   #declare I=I+1;
#end

// Ciclo di disegno
#declare I=0;
#while (I<=8)
   #declare J=0;
   #while (J<16)
      cylinder { Points[I][J], Points[I][mod(J+1,16)], 0.02 
         no_shadow texture {txtWireFrame} }
      cylinder { Points[I][J], Points[mod(I+1,8)][J], 0.02 
         no_shadow texture {txtWireFrame} }
      #declare J=J+1;
   #end
   #declare I=I+1;
#end

Versione retro' della superficie


Appendici

Questo file macro, la documentazione e i disegni allegati sono copyright 1999 di Daniele Varrazzo.

Potete usare liberamente questo programma, nonchè copiarlo e/o modificarlo.

Se intendete distribuire il programma, prima contattatemi, in modo che vi possa fornire la versione più aggiornata.

Questo programma ve lo dò così com'è: non c'è nessuna garanzia che possa essere gradito nè utile e non sono responsabile per qualunque danno possa arrecarvi, inclusi divorzi, mal di denti, forature di gomme, invasioni aliene.

POV-Ray™ e Persistence of Vision™ sono marchi registrati del POV-Ray Team™.

Il file macro "Compressed Mesh" è © di Chris Colefax.