Che cos'è lo stato dell'arte per il rendering di testo in OpenGL dalla versione 4.1? [chiuso]


199

Esistono già alcune domande sul rendering del testo in OpenGL, come:

Ma soprattutto ciò che viene discusso è il rendering di quad testurizzati usando la pipeline a funzione fissa. Sicuramente gli shader devono fare un modo migliore.

Non sono veramente preoccupato per l'internazionalizzazione, la maggior parte delle mie stringhe saranno etichette di tick di trama (data e ora o puramente numeriche). Ma i grafici verranno nuovamente renderizzati alla frequenza di aggiornamento dello schermo e potrebbe esserci un bel po 'di testo (non più di qualche migliaio di glifi sullo schermo, ma abbastanza che il layout con accelerazione hardware sarebbe bello).

Qual è l'approccio raccomandato per il rendering del testo usando OpenGL moderno? (Citando il software esistente usando l'approccio è una buona prova che funziona bene)

  • Shader di geometria che accettano ad es. Posizione e orientamento e una sequenza di caratteri ed emettono quadratini strutturati
  • Shader di geometria che rendono i caratteri vettoriali
  • Come sopra, ma usando invece shader di tassellatura
  • Uno shader di calcolo per eseguire la rasterizzazione dei caratteri

10
Non sono in grado di rispondere allo stato dell'arte, essendo principalmente orientato a OpenGL ES al giorno d'oggi, ma tessellando un TTF usando il tassellatore GLU e inviandolo come geometria tramite la vecchia pipeline a funzionalità fissa con crenatura calcolata sulla CPU ha dato buoni risultati visivi su anti -aliasing dell'hardware e buone prestazioni su tutta la linea anche quasi un decennio fa. Quindi non è solo con gli shader che puoi trovare un modo "migliore" (ovviamente in base ai tuoi criteri). FreeType può sputare i confini del glifo di Bezier e le informazioni di crenatura, in modo da poter lavorare dal vivo da un TTF in fase di esecuzione.
Tommy

QML2 (di Qt5) esegue alcuni trucchi interessanti con OpenGL e campi di distanza durante il rendering del testo: blog.qt.digia.com/blog/2012/08/08/native-looking-text-in-qml-2
mlvljr

Quindi non lo perdo di nuovo, ecco una libreria che implementa il metodo del campo di distanza di Valve. code.google.com/p/glyphy Non l'ho provato. Forse vale la pena dare un'occhiata: code.google.com/p/signed-distance-field-font-generator
Timmmm

7
questo "off-topic" è la maledizione dell'overflow dello stack. sul serio?
Nikolaos Giotis,

Risposte:


202

I contorni di rendering, a meno che non si visualizzi solo una dozzina di caratteri in totale, rimangono un "non andare" a causa del numero di vertici necessari per carattere per approssimare la curvatura. Sebbene ci siano stati approcci per valutare curve più bezier nel pixel shader, questi soffrono di non essere facilmente antialiasati, il che è banale usando un quad strutturato in una mappa di distanza, e la valutazione delle curve nello shader è ancora molto più computazionale del necessario.

Il miglior compromesso tra "veloce" e "qualità" sono ancora i quad con texture con una trama del campo a distanza firmata. È leggermente più lento rispetto all'uso di un normale quad testurizzato normale, ma non così tanto. D'altra parte, la qualità è in un campo da gioco completamente diverso. I risultati sono davvero sorprendenti, è il più veloce possibile e anche effetti come il bagliore sono banalmente facili da aggiungere. Inoltre, se necessario, è possibile eseguire il downgrade della tecnica su hardware più vecchio.

Vedi la famosa carta Valve per la tecnica.

La tecnica è concettualmente simile a come funzionano le superfici implicite (metaballs e simili), sebbene non generi poligoni. Funziona interamente nel pixel shader e prende la distanza campionata dalla trama come una funzione di distanza. Tutto al di sopra di una soglia scelta (di solito 0,5) è "in", tutto il resto è "out". Nel caso più semplice, su hardware di 10 anni non compatibile con shader, l'impostazione della soglia del test alfa su 0,5 farà esattamente ciò (anche se senza effetti speciali e antialiasing).
Se si desidera aggiungere un po 'più di peso al carattere (finto grassetto), una soglia leggermente più piccola farà il trucco senza modificare una singola riga di codice (basta cambiare l'uniforme "font_weight"). Per un effetto bagliore, si considera semplicemente tutto "al di sopra della soglia" come "in" e tutto al di sopra di un'altra soglia (più piccola) come "out, ma in bagliore" e LERP tra i due. L'antialias funziona allo stesso modo.

Utilizzando un valore di distanza con segno a 8 bit anziché un singolo bit, questa tecnica aumenta la risoluzione effettiva della mappa della trama di 16 volte in ogni dimensione (anziché in bianco e nero, vengono utilizzate tutte le sfumature possibili, quindi abbiamo 256 volte il informazioni utilizzando la stessa memoria). Ma anche se ingrandisci ben oltre il 16x, il risultato sembra ancora abbastanza accettabile. Le lunghe linee rette alla fine diventeranno un po 'irregolari, ma non ci saranno manufatti di campionamento "blocchi".

È possibile utilizzare uno shader di geometria per generare i quad da punti (ridurre la larghezza di banda del bus), ma onestamente i guadagni sono piuttosto marginali. Lo stesso vale per il rendering dei personaggi istanziati come descritto in GPG8. Il sovraccarico dell'istanziamento viene ammortizzato solo se hai molto testo da disegnare. I guadagni non sono, secondo me, in relazione alla complessità aggiunta e alla non declassabilità. Inoltre, sei limitato dalla quantità di registri costanti o devi leggere da un oggetto buffer di trama, che non è ottimale per la coerenza della cache (e l'intento era di ottimizzare per cominciare!).
Un semplice, semplice vecchio buffer di vertici è altrettanto veloce (forse più veloce) se pianifichi l'upload un po 'in anticipo nel tempo e funzionerà su ogni hardware costruito negli ultimi 15 anni. Inoltre, non è limitato a un determinato numero di caratteri nel font, né a un determinato numero di caratteri da visualizzare.

Se sei sicuro di non avere più di 256 caratteri nel tuo carattere, le matrici di trama possono valere la pena di prendere in considerazione la possibilità di eliminare la larghezza di banda del bus in un modo simile alla generazione di quad da punti nello shader della geometria. Quando si utilizza una trama array, le coordinate della trama di tutti i quadricipiti hanno identiche, costanti se tcoordinate e differiscono solo nella rcoordinata, che è uguale all'indice dei caratteri da renderizzare.
Ma come con le altre tecniche, i guadagni previsti sono marginali a costo di incompatibilità con l'hardware della generazione precedente.

C'è uno strumento utile di Jonathan Dummer per generare trame a distanza: pagina di descrizione

Aggiornamento:
come più recentemente sottolineato in Programmable Vertex Pulling (D. Rákos, "OpenGL Insights", pagg. 239), non vi è alcuna latenza o sovraccarico significativo associato all'estrazione di dati di vertici a livello di codice dallo shader sulle nuove generazioni di GPU, rispetto a fare lo stesso usando la funzione fissa standard.
Inoltre, le ultime generazioni di GPU hanno cache L2 per scopi generali sempre più ragionevolmente dimensionate (ad esempio 1536 kB su NVIDIA Kepler), quindi ci si può aspettare il problema di accesso incoerente quando si eseguono offset casuali per gli angoli quad da una trama buffer essendo meno di un problema.

Ciò rende più attraente l'idea di estrarre dati costanti (come le dimensioni quadruple) da una trama buffer. Un'implementazione ipotetica potrebbe quindi ridurre al minimo i trasferimenti di memoria e PCIe, nonché la memoria GPU, con un approccio come questo:

  • Carica solo un indice di caratteri (uno per carattere da visualizzare) come unico input per uno shader di vertice che passa su questo indice e gl_VertexID, e amplificalo a 4 punti nello shader di geometria, avendo ancora l'indice di caratteri e l'id del vertice (questo sarà "gl_primitiveID reso disponibile nel vertex shader") come unico attributo e acquisirà questo tramite feedback di trasformazione.
  • Questo sarà veloce, perché ci sono solo due attributi di output (collo di bottiglia principale in GS), ed è vicino a "no-op" altrimenti in entrambe le fasi.
  • Associa una trama buffer che contiene, per ciascun carattere del font, le posizioni dei vertici del quad testurizzato rispetto al punto base (queste sono fondamentalmente le "metriche del font"). Questi dati possono essere compressi in 4 numeri per quadruplo memorizzando solo l'offset del vertice in basso a sinistra e codificando la larghezza e l'altezza del riquadro allineato all'asse (supponendo che mezzo float, saranno 8 byte di buffer costante per carattere - un tipico carattere di 256 caratteri potrebbe adattarsi completamente a 2 kB di cache L1).
  • Imposta un'uniforme per la linea di base
  • Associare una trama buffer con offset orizzontali. Probabilmente questi potrebbero anche essere calcolati sulla GPU, ma è molto più semplice ed efficiente per quel tipo di cose sulla CPU, in quanto si tratta di un'operazione strettamente sequenziale e per nulla banale (si pensi alla crenatura). Inoltre, sarebbe necessario un altro pass di feedback, che sarebbe un altro punto di sincronizzazione.
  • Rendering dei dati precedentemente generati dal buffer di feedback, lo shader di vertici estrae l'offset orizzontale del punto base e gli offset dei vertici degli angoli dagli oggetti buffer (usando l'id primitivo e l'indice dei caratteri). L'ID vertice originale dei vertici inviati è ora il nostro "ID primitivo" (ricorda che la GS ha trasformato i vertici in quad).

In questo modo, si potrebbe ridurre idealmente la banda di vertici richiesta del 75% (ammortizzata), sebbene sarebbe in grado di eseguire il rendering di una sola riga. Se si desidera essere in grado di eseguire il rendering di più righe in una chiamata di disegno, è necessario aggiungere la linea di base alla trama del buffer, anziché utilizzare un'uniforme (riducendo i guadagni della larghezza di banda).

Tuttavia, anche ipotizzando una riduzione del 75%, poiché i dati dei vertici per visualizzare quantità "ragionevoli" di testo sono solo di circa 50-100 kB (che è praticamente zeroa una GPU o un bus PCIe) - dubito ancora che la complessità aggiunta e la perdita di compatibilità con le versioni precedenti valga davvero la pena. Ridurre lo zero del 75% è ancora solo zero. Devo ammettere che non ho provato l'approccio di cui sopra e sarebbero necessarie ulteriori ricerche per fare una dichiarazione veramente qualificata. Tuttavia, a meno che qualcuno non possa dimostrare una differenza di prestazioni davvero sbalorditiva (usando quantità "normali" di testo, non miliardi di caratteri!), Il mio punto di vista rimane che per i dati dei vertici, un semplice e semplice vecchio buffer di vertici è giustamente abbastanza buono essere considerato parte di una "soluzione all'avanguardia". È semplice e diretto, funziona e funziona bene.

Dopo aver già fatto riferimento a " OpenGL Insights " sopra, vale la pena sottolineare anche il capitolo "Rendering di forme 2D per campi di distanza" di Stefan Gustavson che spiega in dettaglio i rendering di campi di distanza.

Aggiornamento 2016:

Nel frattempo, esistono diverse tecniche aggiuntive che mirano a rimuovere gli artefatti di arrotondamento degli angoli che diventano inquietanti a ingrandimenti estremi.

Un approccio utilizza semplicemente i campi di pseudo-distanza anziché i campi di distanza (la differenza è che la distanza è la distanza più breve non rispetto al contorno reale, ma al contorno o a una linea immaginaria che sporge sul bordo). Questo è un po 'meglio e funziona alla stessa velocità (identico shader), usando la stessa quantità di memoria di trama.

Un altro approccio utilizza la mediana dei tre in una trama a tre canali dettagli e implementazione disponibili su github . L'obiettivo è quello di migliorare gli e-o gli hack utilizzati in precedenza per risolvere il problema. Di buona qualità, leggermente, quasi in modo evidente, più lento, ma utilizza una memoria texture tre volte superiore. Inoltre, gli effetti extra (ad es. Bagliore) sono più difficili da ottenere.

Infine, la memorizzazione delle curve di Bezier reali che compongono i personaggi e la loro valutazione in uno shader di frammenti è diventata pratica , con prestazioni leggermente inferiori (ma non tanto che è un problema) e risultati sorprendenti anche ai massimi ingrandimenti.
Demo WebGL che rende un grande PDF con questa tecnica in tempo reale disponibile qui .


1
Sembrano abbastanza buoni (anche con un filtro ingenuo e in assenza di mipmapping, poiché hai trame molto piccole e i dati si interpolano bene). Personalmente penso che abbiano persino un aspetto migliore della cosa "reale" in molti casi, perché non ci sono stranezze come un accenno, che spesso producono cose che percepisco come "strane". Ad esempio, un testo più piccolo non diventa improvvisamente in grassetto senza una ragione ovvia, né passa ai confini dei pixel - effetti che vedi spesso con caratteri "reali". Potrebbero esserci ragioni storiche per questo (display del 1985 in b / n), ma oggi è al di là della mia comprensione il motivo per cui deve essere così.
Damon,

2
Funziona e sembra fantastico, grazie per la condivisione! Per coloro che desiderano la fonte HLSL frag shader vedere qui . È possibile adattare questo per GLSL sostituendo la clip(...)linea con if (text.a < 0.5) {discard;}(o text.a < threshold). HTH.
Ingegnere

1
Grazie per l'aggiornamento. Vorrei poter votare di nuovo.
Ben Voigt,

2
@NicolBolas: sembra che tu non abbia letto molto attentamente. Entrambe le domande sono spiegate nella risposta. Keplero viene fornito come un esempio di "ultima generazione", non esiste un secondo passaggio (ed è spiegato perché), e dichiaro che non credo che l'ipotetica tecnica di risparmio della larghezza di banda sia notevolmente più veloce o che valga la pena. Tuttavia, la convinzione non significa nulla - si dovrebbe provare a sapere (non lo faccio dal momento che non considero disegnare un "collo di bottiglia" di quantità "normali" di testo). Si potrebbe , tuttavia, essere utile in un quando uno è disperato circa la larghezza di banda e ha importi "anomale" del testo.
Damon,

3
@NicolBolas: hai ragione su quella frase, scusa. È davvero un po 'fuorviante. Nel paragrafo precedente, ho scritto "Probabilmente si potrebbe anche generare questo sulla GPU, ma ciò richiederebbe feedback e ... isnogud." - ma poi ha continuato erroneamente con "i dati generati dal buffer di feedback" . Lo correggerò. In realtà, riscriverò la cosa completa nel fine settimana, quindi è meno ambigua.
Damon,

15

http://code.google.com/p/glyphy/

La differenza principale tra GLyphy e altri renderer OpenGL basati su SDF è che la maggior parte degli altri progetti campiona l'SDF in una trama. Questo ha tutti i soliti problemi che ha il campionamento. Vale a dire. distorce il contorno ed è di bassa qualità. GLyphy rappresenta invece l'SDF utilizzando vettori effettivi inviati alla GPU. Ciò si traduce in un rendering di altissima qualità.

Il rovescio della medaglia è che il codice è per iOS con OpenGL ES. Probabilmente realizzerò una porta OpenGL 4.x per Windows / Linux (speriamo che l'autore aggiunga della vera documentazione, però).


3
Chiunque sia interessato a GLyphy dovrebbe probabilmente guardare il discorso dell'autore su Linux.conf.au 2014: youtube.com/watch?v=KdNxR5V7prk
Fizz

14

La tecnica più diffusa è ancora quella dei quadricromia. Tuttavia, nel 2005 LORIA ha sviluppato qualcosa chiamato trame vettoriali, ovvero il rendering di grafica vettoriale come trame su primitivi. Se uno lo utilizza per convertire i caratteri TrueType o OpenType in una trama vettoriale, ottieni questo:

http://alice.loria.fr/index.php/publications.html?Paper=VTM@2005


2
Conosci qualche implementazione che utilizza questa tecnica?
Luca,

2
No (come in produzione), ma l'articolo di Kilgard (vedi la mia risposta sotto per il link) ha una breve critica, che riassumo come: non ancora pratico. C'è stata più ricerca nell'area; i lavori più recenti citati da Kilgard includono research.microsoft.com/en-us/um/people/hoppe/ravg.pdf e uwspace.uwaterloo.ca/handle/10012/4262
Fizz

9

Sono sorpreso che il bambino di Mark Kilgard, NV_path_rendering (NVpr), non sia stato menzionato da nessuno dei precedenti. Sebbene i suoi obiettivi siano più generali del rendering dei caratteri, può anche eseguire il rendering del testo dai caratteri e con crenatura. Non richiede nemmeno OpenGL 4.1, ma al momento è un'estensione solo fornitore / Nvidia. Fondamentalmente trasforma i caratteri in percorsi usando ciò glPathGlyphsNVche dipende dalla libreria freetype2 per ottenere le metriche, ecc. Quindi puoi anche accedere alle informazioni di crenatura con glGetPathSpacingNVe usare il meccanismo generale di rendering del percorso di NVpr per visualizzare il testo usando i caratteri "convertiti". (Lo metto tra virgolette, poiché non esiste una vera conversione, le curve vengono utilizzate così come sono.)

La demo registrato per le capacità dei font di NVpr purtroppo non è particolarmente impressionante. (Forse qualcuno dovrebbe crearne uno sulla falsariga della demo più snazzi di SDF che si può trovare sugli intertubes ...)

Il discorso di presentazione dell'API NVpr 2011 per la parte font inizia qui e continua nella parte successiva ; è un po 'sfortunato come questa presentazione sia divisa.

Materiali più generali su NVpr:

  • Hub Nvidia NVpr , ma un po 'di materiale sulla pagina di destinazione non è il più aggiornato
  • Carta Siggraph 2012 per il cervello del metodo di rendering del percorso, chiamato "stencil, then cover" (StC); l'articolo spiega inoltre brevemente come funzionano le tecnologie concorrenti come Direct2D. I bit relativi al carattere sono stati relegati in un allegato del documento . Ci sono anche alcuni extra come video / demo .
  • Presentazione GTC 2014 per uno stato di aggiornamento; in breve: ora è supportato da Google Skia (Nvidia ha contribuito al codice alla fine del 2013 e 2014), che a sua volta viene utilizzato in Google Chrome e [indipendentemente da Skia, penso] in una beta di Adobe Illustrator CC 2014
  • la documentazione ufficiale nel registro delle estensioni OpenGL
  • USPTO ha concesso almeno quattro brevetti a Kilgard / Nvidia in relazione a NVpr, di cui dovresti probabilmente essere a conoscenza, nel caso in cui desideri implementare StC da solo: US8698837 , US8698808 , US8704830 e US8730253 . Si noti che esistono altri 17 documenti USPTO collegati a questo come "anche pubblicati come", la maggior parte dei quali sono domande di brevetto, quindi è del tutto possibile che possano essere concessi più brevetti da quelli.

E poiché la parola "stencil" non ha prodotto risultati in questa pagina prima della mia risposta, sembra che il sottoinsieme della comunità SO che ha partecipato a questa pagina nella misura in cui, nonostante sia piuttosto numeroso, non era a conoscenza di tessellazioni, stencil-buffer- metodi basati per il rendering di percorsi / font in generale. Kilgard ha un post simile a FAQ sul forum di Opengl che potrebbe chiarire come i metodi di rendering del percorso senza tassellazioni differiscono dalla grafica 3D standard di torbiera, anche se stanno ancora usando una GPU [GP]. (NVpr ha bisogno di un chip compatibile con CUDA.)

Dal punto di vista storico, Kilgard è anche l'autore del classico "Un'API semplice basata su OpenGL per Texture Mapped Text", SGI, 1997 , che non deve essere confuso con lo NVpr basato su stencil che ha debuttato nel 2011.


La maggior parte se non tutti i metodi recenti discussi in questa pagina, compresi i metodi basati su stencil come NVpr o metodi basati su SDF come GLyphy (di cui non sto discutendo ulteriormente perché altre risposte già lo coprono) hanno comunque una limitazione: sono adatto per la visualizzazione di testo di grandi dimensioni su monitor convenzionali (~ 100 DPI) senza jaggies a qualsiasi livello di ridimensionamento, e hanno anche un bell'aspetto, anche di piccole dimensioni, su display ad alta DPI, simili a retina. Tuttavia, non forniscono completamente ciò che Direct2D + DirectWrite di Microsoft offre, in particolare suggerimenti di piccoli glifi sui display tradizionali. (Per un sondaggio visivo sul suggerimento in generale, vedi questa pagina di esempio per esempio. Una risorsa più approfondita è su antigrain.com .)

Non sono a conoscenza di elementi aperti e produttivi basati su OpenGL che possano fare ciò che Microsoft può fare con i suggerimenti al momento. (Ammetto l'ignoranza degli interni di OS X GL / Quartz di Apple, perché per quanto ne so Apple non ha pubblicato come fanno cose di rendering di font / percorsi basati su GL. Sembra che OS X, a differenza di MacOS 9, non lo faccia suggerire del tutto, il che infastidisce alcune persone .) Comunque, c'è un documento di ricerca del 2013 che affronta i suggerimenti tramite shader OpenGL scritti da Nicolas P. Rougier di INRIA; vale probabilmente la pena leggere se è necessario fare un suggerimento da OpenGL. Mentre può sembrare che una libreria come il freetype faccia già tutto il lavoro quando si tratta di suggerimenti, non è in realtà così per il seguente motivo, che sto citando dal documento:

La libreria FreeType può rasterizzare un glifo usando l'antialiasing sub-pixel in modalità RGB. Tuttavia, questo è solo metà del problema, poiché vogliamo anche ottenere un posizionamento sub-pixel per un posizionamento accurato dei glifi. La visualizzazione del quad testurizzato a coordinate di pixel frazionari non risolve il problema, poiché comporta solo l'interpolazione delle trame a livello di pixel interi. Invece, vogliamo ottenere uno spostamento preciso (tra 0 e 1) nel dominio subpixel. Questo può essere fatto in uno shader di frammenti [...].

La soluzione non è esattamente banale, quindi non proverò a spiegarla qui. (Il documento è ad accesso aperto.)


Un'altra cosa che ho imparato dall'articolo di Rougier (e che Kilgard non sembra aver preso in considerazione) è che i poteri dei caratteri che sono (Microsoft + Adobe) hanno creato non uno ma due metodi di specificazione della crenatura. Quello vecchio si basa su una cosiddetta tabella kern ed è supportato da freetype. Il nuovo si chiama GPOS ed è supportato solo dalle più recenti librerie di font come HarfBuzz o Pango nel mondo del software libero. Poiché NVpr non sembra supportare nessuna di queste librerie, la crenatura potrebbe non funzionare con NVpr per alcuni nuovi caratteri; ce ne sono alcuni apparentemente allo stato brado, secondo questo forum di discussione .

Infine, se hai bisogno di fare un layout di testo complesso (CTL) , sembra che tu sia attualmente sfortunato con OpenGL poiché non sembra esistere alcuna libreria basata su OpenGL. (DirectWrite d'altra parte può gestire CTL.) Ci sono librerie open source come HarfBuzz che possono rendere CTL, ma non so come farle funzionare bene (come usando i metodi basati su stencil) tramite OpenGL. Probabilmente dovresti scrivere il codice della colla per estrarre i contorni riformati e inserirli in soluzioni basate su NVpr o SDF come percorsi.


4
Non ho menzionato NV_path_rendering perché è un'estensione, un fornitore proprietario per peggiorare le cose. Di solito cerco di dare risposte solo per tecniche universalmente applicabili.
datenwolf,

1
Bene, posso essere d'accordo con quello in una certa misura. Il metodo stesso ("stencil, then cover") non è in realtà difficile da implementare direttamente in OpenGL, ma avrà un elevato sovraccarico di comando se fatto ingenuamente in quel modo, poiché i precedenti tentativi basati su stencil finivano. Skia [via Ganesh] ha provato una soluzione basata su stencil sul punto, ma ha rinunciato ad essa, secondo Kilgrad. Il modo in cui è implementato da Nvidia, un livello sottostante, che utilizza le funzionalità CUDA, lo rende performante. Puoi provare a "Mantle" StC usando un intero gruppo di estensioni EXT / ARB. Ma attenzione che Kilgard / Nvidia hanno due domande di brevetto su NVpr.
Fizz

3

Penso che la tua scommessa migliore sarebbe quella di esaminare la grafica cairo con il backend OpenGL.

L'unico problema che ho avuto durante lo sviluppo di un prototipo con 3.3 core era l'utilizzo delle funzioni obsolete nel backend OpenGL. Era 1-2 anni fa, quindi la situazione potrebbe essere migliorata ...

Ad ogni modo, spero che in futuro i driver grafici desktop opengl implementeranno OpenVG.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.