Renderizza un globo SVG

Cosa starai creando

In questo tutorial, ti mostrerò come prendere una mappa SVG e proiettarla su un globo, come un vettore. Per realizzare le trasformazioni matematiche necessarie per proiettare la mappa su una sfera, dobbiamo usare lo scripting Python per leggere i dati della mappa e tradurli in un'immagine di un globo. Questo tutorial presume che tu stia utilizzando Python 3.4, l'ultimo Python disponibile.

Inkscape ha una sorta di API Python che può essere usata per fare una varietà di cose. Tuttavia, dal momento che siamo interessati solo alla trasformazione delle forme, è più semplice scrivere un programma autonomo che legge e stampa i file SVG da solo.

1. Formattare la mappa

Il tipo di mappa che vogliamo è chiamato una mappa equirettangolare. In una mappa equirettangolare, la longitudine e la latitudine di un luogo corrisponde alla sua X e y posizione sulla mappa. Una mappa del mondo equirettangolare può essere trovata su Wikimedia Commons (qui c'è una versione con stati degli Stati Uniti).

Le coordinate SVG possono essere definite in vari modi. Ad esempio, possono essere relativi al punto definito in precedenza o definiti in modo assoluto dall'origine. Per semplificarci le nostre vite, vogliamo convertire le coordinate nella mappa nella forma assoluta. Inkscape può farlo. Vai alle preferenze di Inkscape (sotto modificare menu) e sotto Input Output > Uscita SVG, impostato Formato della stringa di percorso a Assoluto.

Inkscape non convertirà automaticamente le coordinate; devi eseguire una sorta di trasformazione sui percorsi per farlo accadere. Il modo più semplice per farlo è semplicemente selezionare tutto e spostarlo verso l'alto e verso il basso con una pressione ciascuna delle frecce su e giù. Quindi ri-salva il file.

2. Avvia il tuo script Python

Crea un nuovo file Python. Importa i seguenti moduli:

import sys import re importare matematica tempo di importazione import datetime import numpy come np import xml.etree.ElementTree as ET

Dovrai installare NumPy, una libreria che ti consente di eseguire determinate operazioni vettoriali come prodotti a punti e prodotti incrociati.

3. The Math of Perspective Projection

Proiettare un punto nello spazio tridimensionale in un'immagine 2D implica trovare un vettore dalla telecamera al punto e quindi suddividere quel vettore in tre vettori perpendicolari. 

I due vettori parziali perpendicolari al vettore della telecamera (la direzione verso cui è rivolta la telecamera) diventano i X e y coordinate di un'immagine ortogonalmente proiettata. Il vettore parziale parallelo al vettore della fotocamera diventa qualcosa chiamato il z distanza del punto. Per convertire un'immagine ortogonale in un'immagine prospettica, dividere ciascuno X e y coordinato dal z distanza.

A questo punto, ha senso definire determinati parametri della telecamera. Innanzitutto, dobbiamo sapere dove si trova la fotocamera nello spazio 3D. Conserva il suo X, y, e z coordinate in un dizionario.

camera = 'x': -15, 'y': 15, 'z': 30

Il globo si troverà all'origine, quindi ha senso orientare la fotocamera verso di essa. Ciò significa che il vettore di direzione della telecamera sarà l'opposto della posizione della telecamera.

cameraForward = 'x': -1 * camera ['x'], 'y': -1 * camera ['y'], 'z': -1 * camera ['z']

Non basta solo stabilire in quale direzione si trova la fotocamera, è necessario anche fissare una rotazione per la fotocamera. Fatelo definendo un vettore perpendicolare al cameraForward vettore.

cameraPerpendicular = 'x': cameraForward ['y'], 'y': -1 * cameraForward ['x'], 'z': 0

1. Definire le funzioni vettoriali utili

Sarà molto utile avere determinate funzioni vettoriali definite nel nostro programma. Definire una funzione di magnitudine vettoriale:

#magnitude di un vettore 3D def sumOfSquares (vector): vettore di ritorno ['x'] ** 2 + vettore ['y'] ** 2 + vettore ['z'] ** 2 magnitudine di def (vettore): matematica di ritorno .sqrt (sumOfSquares (vettore))

Dobbiamo essere in grado di proiettare un vettore su un altro. Poiché questa operazione implica un prodotto con punti, è molto più semplice utilizzare la libreria NumPy. NumPy, tuttavia, prende i vettori in forma di lista, senza gli identificatori espliciti 'x', 'y', 'z', quindi abbiamo bisogno di una funzione per convertire i nostri vettori in vettori NumPy.

#converts dizionario vector to list vector def vectorToList (vector): return [vector ['x'], vector ['y'], vector ['z']]
#projects u su v def vectorProject (u, v): return np.dot (vectorToList (v), vectorToList (u)) / magnitude (v)

È bello avere una funzione che ci darà un vettore unitario nella direzione di un dato vettore:

#get unità vector def unitVector (vettore): magVector = magnitude (vector) return 'x': vector ['x'] / magVector, 'y': vector ['y'] / magVector, 'z': vector [ 'z'] / magVector

Infine, dobbiamo essere in grado di prendere due punti e trovare un vettore tra loro:

# Calcola il vettore da due punti, dizionario forma def findVector (origine, punto): return 'x': point ['x'] - origine ['x'], 'y': punto ['y'] - origine [ 'y'], 'z': punto ['z'] - origine ['z']

2. Definire gli assi della videocamera

Ora dobbiamo solo completare la definizione degli assi della telecamera. Abbiamo già due di questi assi-cameraForward e cameraPerpendicular, corrispondente al z distanza e X coordinata dell'immagine della telecamera. 

Ora abbiamo solo bisogno del terzo asse, definito da un vettore che rappresenta il y coordinata dell'immagine della telecamera. Possiamo trovare questo terzo asse prendendo il prodotto incrociato di quei due vettori, usando NumPy-np.cross (vectorToList (cameraForward), vectorToList (cameraPerpendicular)).

Il primo elemento nel risultato corrisponde al X componente; il secondo al y componente, e il terzo al z componente, quindi il vettore prodotto è dato da:

# Calcola vettore piano orizzonte (punti verso l'alto) cameraHorizon = 'x': np.cross (vectorToList (cameraForward), vectorToList (cameraPerpendicular)) [0], 'y': np.cross (vectorToList (cameraForward), vectorToList (cameraPerpendicular )) [1], 'z': np.cross (vectorToList (cameraForward), vectorToList (cameraPerpendicular)) [2]

3. Progetto a Orthogonal

Per trovare l'ortogonale X, y, e z distanza, per prima cosa troviamo il vettore che collega la camera e il punto in questione, quindi lo proiettiamo su ciascuno dei tre assi della telecamera definiti in precedenza:

def physicalProjection (point): pointVector = findVector (camera, punto) #pointVector è un vettore che inizia dalla telecamera e termina in un punto in questione return 'x': vectorProject (pointVector, cameraPerpendicular), 'y': vectorProject (pointVector , cameraHorizon), 'z': vectorProject (pointVector, cameraForward)

Un punto (grigio scuro) proiettato sui tre assi della telecamera (grigio). X è rosso, y è verde, e z è blu.

4. Progetto in prospettiva

La proiezione prospettica prende semplicemente il X e y della proiezione ortogonale e divide ciascuna coordinata dal z distanza. Questo rende così più roba più lontana rispetto a cose più vicine alla fotocamera. 

Perché dividersi da z produce coordinate molto piccole, moltiplichiamo ogni coordinata di un valore corrispondente alla lunghezza focale della telecamera.

focalLength = 1000
# disegna punti sul sensore della videocamera usando xDistanza, Distanza e zDistanza defproiezione di prospettiva (pCoords): scaleFactor = focalLength / pCoords ['z'] return 'x': pCoords ['x'] * scaleFactor, 'y': pCoords [ 'y'] * scaleFactor

5. Converti coordinate sferiche in coordinate rettangolari

La Terra è una sfera. Quindi le nostre coordinate - latitudine e longitudine - sono coordinate sferiche. Quindi abbiamo bisogno di scrivere una funzione che converta le coordinate sferiche in coordinate rettangolari (così come definire un raggio della Terra e fornire π costante):

raggio = 10 pi = 3.14159
# converte le coordinate sferiche in coordinate rettangolari def sphereToRect (r, a, b): return 'x': r * math.sin (b * pi / 180) * math.cos (a * pi / 180), 'y' : r * math.sin (b * pi / 180) * math.sin (a * pi / 180), 'z': r * math.cos (b * pi / 180)

Possiamo ottenere prestazioni migliori memorizzando alcuni calcoli usati più volte:

# converte le coordinate sferiche in coordinate rettangolari def sphereToRect (r, a, b): aRad = math.radians (a) bRad = math.radians (b) r_sin_b = r * math.sin (bRad) return 'x': r_sin_b * math.cos (aRad), 'y': r_sin_b * math.sin (aRad), 'z': r * math.cos (bRad)

Possiamo scrivere alcune funzioni composite che combineranno tutti i passaggi precedenti in un'unica funzione, andando direttamente dalle coordinate sferiche o rettangolari alle immagini prospettiche:

#functions for plotting points def rectPlot (coordinate): return perspectiveProjection (physicalProjection (coordinate)) def spherePlot (coordinate, sRadius): return rectPlot (sphereToRect (sRadius, coordinate ['long'], coordinate ['lat'])))

4. Rendering in SVG

Il nostro script deve essere in grado di scrivere su un file SVG. Quindi dovrebbe iniziare con:

f = open ('globe.svg', 'w') f.write ('\ n\ N ')

E finire con:

f.write ('')

Produrre un file SVG vuoto ma valido. All'interno di tale file lo script deve essere in grado di creare oggetti SVG, quindi definiremo due funzioni che gli consentiranno di disegnare punti e poligoni SVG:

#Draws SVG circle object def svgCircle (coordinate, circleRadius, colore): f.write ('\ n ') #Draws SVG poligono nodo def polyNode (coordinata): f.write (str (coordinata [' x '] + 400) +', '+ str (coordinata [' y '] + 400) + ")

Possiamo testare questo mostrando una griglia di punti sferica:

#DRAW GRID per x nel range (72): per y nel range (36): svgCircle (spherePlot ('long': 5 * x, 'lat': 5 ​​* y, raggio), 1, '#ccc' )

Questo script, una volta salvato ed eseguito, dovrebbe produrre qualcosa del genere:


5. Trasforma i dati della mappa SVG

Per leggere un file SVG, uno script deve essere in grado di leggere un file XML, poiché SVG è un tipo di XML. Ecco perché abbiamo importato xml.etree.ElementTree. Questo modulo ti consente di caricare XML / SVG in uno script come elenco annidato:

tree = ET.parse ('BlankMap Equirectangular states.svg') root = tree.getroot ()

È possibile navigare verso un oggetto nell'SVG attraverso gli indici delle liste (in genere è necessario dare un'occhiata al codice sorgente del file della mappa per comprenderne la struttura). Nel nostro caso, ogni paese si trova a radice [4] [0] [X] [n], dove X è il numero del paese, a partire da 1, e n rappresenta i vari sottotraccia che delineano il paese. I contorni effettivi del paese sono memorizzati nel d attributo, accessibile attraverso radice [4] [0] [X] [n] .Attrib [ 'd'].

1. Costruire loop

Non possiamo semplicemente scorrere questa mappa perché contiene un elemento "fittizio" all'inizio che deve essere saltato. Quindi dobbiamo contare il numero di oggetti "paese" e sottrarne uno per sbarazzarci del manichino. Quindi passiamo in rassegna gli oggetti rimanenti.

countries = len (root [4] [0]) - 1 per x nel range (paesi): root [4] [0] [x + 1]

Alcuni oggetti country includono più percorsi, motivo per cui quindi iteriamo attraverso ogni percorso in ogni paese:

countries = len (root [4] [0]) - 1 per x in range (paesi): per path in root [4] [0] [x + 1]:

All'interno di ogni percorso, ci sono contorni disgiunti separati dai caratteri 'Z M' nel d stringa, quindi abbiamo diviso il d stringa lungo quel delimitatore e itera attraverso quelli.

countries = len (root [4] [0]) - 1 per x nel range (paesi): per path in root [4] [0] [x + 1]: per k in re.split ('Z M', path.attrib [ 'd']):

Quindi suddividiamo ogni contorno in base ai delimitatori "Z", "L" o "M" per ottenere le coordinate di ogni punto nel percorso:

per x nel range (paesi): per path in root [4] [0] [x + 1]: per k in re.split ('Z M', path.attrib ['d']): per i in re .split ('Z | M | L', k):

Quindi rimuoviamo tutti i caratteri non numerici dalle coordinate e li dividiamo a metà lungo le virgole, dando le latitudini e le longitudini. Se entrambi esistono, li memorizziamo in a sphereCoordinates dizionario (nella mappa, le coordinate di latitudine vanno da 0 a 180 °, ma vogliamo che vadano da -90 ° a 90 ° - nord e sud - quindi sottraiamo 90 °).

per x nel range (paesi): per path in root [4] [0] [x + 1]: per k in re.split ('Z M', path.attrib ['d']): per i in re .split ('Z | M | L', k): breakup = re.split (',', re.sub ("[^ - 0123456789.,]", "", i)) se interruzione [0] e breakup [1]: sphereCoordinates =  sphereCoordinates ['long'] = float (breakup [0]) sphereCoordinates ['lat'] = float (breakup [1]) - 90

Quindi se lo proviamo tracciando dei punti (svgCircle (spherePlot (sphereCoordinates, radius), 1, '# 333')), otteniamo qualcosa di simile a questo:

2. Risolvi l'occlusione

Questo non fa distinzione tra punti sul lato vicino del globo e punti sul lato più lontano del globo. Se vogliamo stampare solo punti sul lato visibile del pianeta, dobbiamo essere in grado di capire da che parte del pianeta si trova un determinato punto. 

Possiamo farlo calcolando i due punti sulla sfera in cui un raggio dalla telecamera al punto si interseca con la sfera. Questa funzione implementa la formula per risolvere le distanze da questi due punti-dNear e DFAR:

cameraDistanceSquare = sumOfSquares (fotocamera) #distanza dal centro del globo alla distanza di def cameraToPoint (spherePoint): point = sphereToRect (raggio, spherePoint ['long'], spherePoint ['lat']) ray = findVector (camera, punto) return vectorProject ( ray, cameraForward)
def occlude (spherePoint): point = sphereToRect (raggio, spherePoint ['long'], spherePoint ['lat']) ray = findVector (telecamera, punto) d1 = magnitude (ray) #distanza dalla telecamera al punto dot_l = np. punto ([raggio ['x'] / d1, raggio ['y'] / d1, raggio ['z'] / d1], vectorToList (fotocamera)) #dispositivo prodotto del vettore unitario da fotocamera a punto e determinante del vettore della telecamera = math.sqrt (abs ((dot_l) ** 2 - cameraDistanceSquare + raggio ** 2)) dNear = - (dot_l) + determinante dFar = - (dot_l) - determinante

Se la distanza effettiva al punto, d1, è inferiore o uguale a tutti e due di queste distanze, quindi il punto è sul lato vicino della sfera. A causa degli errori di arrotondamento, in questa operazione è incorporato un piccolo spazio di manovra:

 se d1 - 0,0000000001 <= dNear and d1 - 0.0000000001 <= dFar : return True else: return False

L'utilizzo di questa funzione come condizione dovrebbe limitare il rendering ai punti near-side:

 se occlude (sphereCoordinates): svgCircle (spherePlot (sphereCoordinates, radius), 1, '# 333')

6. Renderizza paesi solidi

Certo, i punti non sono vere forme chiuse e piene - danno solo l'illusione di forme chiuse. Disegnare paesi reali pieni richiede un po 'più di raffinatezza. Prima di tutto, dobbiamo stampare la totalità di tutti i paesi visibili. 

Possiamo farlo creando un interruttore che si attiva ogni volta che un paese contiene un punto visibile, nel frattempo memorizzando temporaneamente le coordinate di quel paese. Se l'interruttore è attivato, il paese viene disegnato, utilizzando le coordinate memorizzate. Disegneremo anche poligoni invece di punti.

per x nell'intervallo (paesi): per il percorso nella radice [4] [0] [x + 1]: per k in re.split ('Z M', path.attrib ['d']): countryIsVisible = False country = [] per i in re.split ('Z | M | L', k): breakup = re.split (',', re.sub ("[^ - 0123456789.,]", "", i) ) se breakup [0] e breakup [1]: sphereCoordinates =  sphereCoordinates ['long'] = float (breakup [0]) sphereCoordinates ['lat'] = float (breakup [1]) - 90 #DRAW COUNTRY se occlude (coordinate sfera): country.append ([sphereCoordinates, radius]) countryIsVisible = True else: country.append ([sphereCoordinates, radius]) se countryIsVisible: f.write ('\ N \ n ')

È difficile dirlo, ma i paesi ai confini del mondo si ripiegano su se stessi, cosa che non vogliamo (dai un'occhiata al Brasile).

1. Traccia il Disco della Terra

Per fare in modo che i paesi vengano visualizzati correttamente ai bordi del globo, dobbiamo prima tracciare il disco del globo con un poligono (il disco che vedete dai punti è un'illusione ottica). Il disco è delineato dal bordo visibile del globo: un cerchio. Le seguenti operazioni calcolano il raggio e il centro di questo cerchio, nonché la distanza del piano che contiene il cerchio dalla telecamera e il centro del globo.

#TRACE LIMB limbRadius = math.sqrt (raggio ** 2 - raggio ** 4 / cameraDistanceSquare) cx = camera ['x'] * raggio ** 2 / cameraDistanceSquare cy = camera ['y'] * raggio ** 2 / cameraDistanceSquare cz = camera ['z'] * raggio ** 2 / cameraDistanceSquare planeDistance = magnitudine (camera) * (1 - raggio ** 2 / cameraDistanceSquare) planeDisplacement = math.sqrt (cx ** 2 + cy ** 2 + cz ** 2)

La terra e la fotocamera (punto grigio scuro) viste dall'alto. La linea rosa rappresenta il bordo visibile della terra. Solo il settore ombreggiato è visibile alla telecamera.

Quindi per rappresentare graficamente un cerchio in quel piano, costruiamo due assi paralleli a quel piano:

#trade e negate xey per ottenere un vettore perpendicolare unitVectorCamera = unitVector (camera) aV = unitVector ('x': -unitVectorCamera ['y'], 'y': unitVectorCamera ['x'], 'z': 0) bV = np.cross (vectorToList (aV), vectorToList (unitVectorCamera))

Quindi tracciamo un grafico su quegli assi con incrementi di 2 gradi per tracciare un cerchio in quel piano con quel raggio e il centro (vedere questa spiegazione per la matematica):

per t in range (180): theta = math.radians (2 * t) cosT = math.cos (theta) sinT = math.sin (theta) limbPoint = 'x': cx + limbRadius * (cosT * aV [ 'x'] + sinT * bV [0]), 'y': cy + limbRadius * (cosT * aV ['y'] + sinT * bV [1]), 'z': cz + limbRadius * (cosT * aV ['z'] + sinT * bV [2])

Quindi incapsuliamo tutto ciò con il codice di disegno poligonale:

f.write ('')

Creiamo anche una copia di tale oggetto da utilizzare in seguito come maschera di ritaglio per tutti i nostri paesi:

f.write ('')

Questo dovrebbe darti questo:

2. Ritaglio sul disco

Usando il disco appena calcolato, possiamo modificare il nostro altro dichiarazione nel codice di tracciatura del paese (per quando le coordinate si trovano sul lato nascosto del globo) per tracciare quei punti da qualche parte al di fuori del disco:

 else: tangentscale = (raggio + planeDisplacement) / (pi * 0.5) rr = 1 + abs (math.tan ((distanceToPoint (sphereCoordinates) - planeDistance) / tangentscale)) country.append ([sphereCoordinates, radius * rr])

Questo utilizza una curva tangente per sollevare i punti nascosti sopra la superficie della Terra, dando l'impressione che siano distribuiti attorno ad esso:

Questo non è del tutto matematicamente valido (si rompe se la telecamera non è puntata approssimativamente al centro del pianeta), ma è semplice e funziona la maggior parte del tempo. Quindi aggiungendo semplicemente clip-path = "url (#clipglobe)" al codice del disegno poligonale, possiamo agevolmente ritagliare i paesi fino al bordo del globo:

 se countryIsVisible: f.write ('

Spero che questo tutorial ti sia piaciuto! Divertiti con i tuoi globi vettoriali!