Livecoding: Programació creativa en temps real
Visuals amb Hydra
Hydra és un sintetitzador de vídeo per a livecoding, escrit en JavaScript i executable directament des d’un navegador web.
|
El JavaScript és un llenguatge de programació que s’utilitza principalment per crear pàgines web interactives. Tots els navegadors web són capaços d’interpretar i executar codi JavaScript. |
Internament, Hydra tradueix el nostre codi en un shader de WebGL, fet que permet executar efectes visuals complexos de forma eficient utilitzant la targeta gràfica de l’ordinador.
|
Un shader és un petit programa que s’executa directament a la targeta gràfica en comptes del processador principal. La targeta gràfica compta amb centenars o milers de petits processadors que poden executar el programa simultàniament per a cada píxel de la pantalla, cosa que fa que l’obtenció del resultat final sigui molt ràpida. WebGL és una API (interfície de programació d’aplicacions) que permet crear gràfics 3D i 2D en un navegador utilitzant shaders. |
Hydra s’inspira en el funcionament dels sintetitzadors modulars. En un sintetitzador modular, un mòdul produeix un senyal, i aquest senyal es condueix mitjançant cables cap a altres mòduls que el modifiquen, per acabar dirigint-lo a una sortida on es pot escoltar el resultat. En sistemes complexos, altres senyals es poden utilitzar per modificar paràmetres dins els mòduls, en un procés que s’anomena modulació, aconseguint que el so resultant sigui dinàmic i vagi mutant amb el temps.
Seguint aquesta lògica, Hydra distingeix tres tipus de funcions:
-
Generadores: creen un resultat visual inicial.
-
Transformadores: modifiquen una imatge existent.
-
De sortida: permeten mostrar el resultat final a la pantalla.
Primers exemples
|
Els exemples d’aquesta guia són interactius. Pots modificar el codi i executar-lo amb el botó ▶ o amb la combinació de tecles |
El primer exemple senzill utilitza un generador, osc(), i l’envia a la sortida amb out():
osc().out()
Totes les funcions s’escriuen amb parèntesis al final del nom. Utilitzem el punt . per enllaçar la sortida d’una funció amb l’entrada de la següent.
La majoria de funcions poden rebre paràmetres per modificar-ne el comportament. Per exemple, podem canviar el valor 10 per altres nombres:
osc(10).out()
Aquest primer paràmetre d'osc() controla la freqüència de l’oscil·lador. Si no s’especifica, Hydra hi assigna un valor per defecte, en aquest cas 60.
osc() accepta fins a tres paràmetres: la freqüència (per defecte 60), la velocitat (0.1) i la desviació (0). La desviació separa les diferents components de color. Pots experimentar amb diferents valors, utilitzant velocitats negatives o augmentant progressivament la desviació:
osc(60, 0.1, 0.1).out()
|
En programació, s’utilitza la notació anglesa per als decimals: el punt |
|
Els colors en un ordinador es formen combinant diferents intensitats de vermell, verd i blau (RGB). El paràmetre de desviació mou la component vermella cap a una banda i la blava cap a l’altra, mentre manté la verda fixa. |
Un cop hem vist com funciona un generador com osc(), podem afegir transformacions. Per exemple, rotate() permet girar el patró generat.
rotate() rep dos paràmetres opcionals: l’angle inicial (en radiants) i la velocitat de rotació.
Prova a modificar els valors d'osc() o rotate() per veure’n el resultat:
osc(50,0).rotate(0, 0.2).out()
|
Pots utilitzar |
Les transformacions es poden encadenar. Al següent exemple, apliquem una rotació i després un efecte de repetició (3 vegades en horitzontal i 3 en vertical):
osc(10).rotate(0,0.1).repeat(3,3).out()
L’ordre en què apliquem les transformacions és important:
osc(10).repeat(3,3).rotate(0,0.1).out()
|
Per escriure més ràpidament, pots ometre el zero inicial en decimals entre 0 i 1: |
L’editor d’Hydra
L’editor oficial d’hydra és https://hydra.ojack.xyz/(en aquesta pàgina web).
Quan hi accedim, es carrega automàticament un patró visual aleatori i es mostra una finestra d’ajuda, que podem tancar clicant la icona .
Adreces de teclat
L’editor ofereix diverses dreceres per facilitar la codificació:
-
Ctrl+Enter: executa una línia de codi. -
Ctrl+Shift+Enter: executa tot el codi del document. -
Alt+Enter: executa un bloc de codi delimitat per línies en blanc (ideal per preparar diferents seccions). -
Ctrl+Shift+H: amaga o mostra el codi per mostrar només el patró visual. -
Ctrl+Shift+F: aplica format automàtic al codi. -
Ctrl+Shift+S: fa una captura de pantalla del patró visual i permet descarregar el codi.
Interfície
A la part superior dreta hi ha diverses icones amb les següents funcionalitats:
-
: executa tot el codi.
-
: neteja la pantalla, esborra el patró actual i tot el codi. Útil per eliminar el patró d’exemple que apareix quan entrem a l’editor i començar des de 0.
-
: permet afegir extensions a Hydra. Les extensions són funcions addicionals que han programat els usuaris i que permeten ampliar les funcionalitats de Hydra.
-
: carrega un patró aleatori. Aquests patrons són contribucions dels usuaris.
-
: fa un canvi aleatori al patró actual. Pot ser interessant per explorar noves possibilitats a partir d’un patró ja existent.
-
: afegeix el patró actual com a contribució a la llista de patrons aleatoris. Un diàleg demanarà un nom o descripció abans de realment compartir-lo.
-
: mostra l’ajuda (la mateixa finestra que s’obre quan entrem a l’editor).
Funcions generadores
A continuació veurem les funcions generadores que ens ofereix Hydra. Per a cadascuna, es mostra el nom, els paràmetres, i els seus valors per defecte (en cas que no l’especifiquem):
-
noise( scale = 10, offset = 0.1 ): genera soroll aleatori.scaleés la mida dels píxels del soroll, ioffsetés la velocitat de canvi.noise(5, 0.3).out() -
voronoi( scale = 5, speed = 0.3, blending = 0.3 ): un patró de Voronoi és una partició de l’espai en regions segons la proximitat a un conjunt de punts. A cada regió se li assigna un color.Scaleés la mida, a més gran més particions,speedés la velocitat de canvi, iblendingcontrola com de suaus són les transicions entre regions (amb 0 veurem perfectament definides les diferents àrees).voronoi(10, 0.3, 0.3).out() -
osc( frequency = 60, sync = 0.1, offset = 0 ): genera un oscil·lador.frequencyés la freqüència de l’oscil·lador,syncés la velocitat de canvi, ioffsetés la desviació dels colors. -
shape( sides = 3, radius = 0.3, smoothing = 0.01 ): un polígon regular.sidesés el nombre de costats,radiusés el radi del polígon, ismoothingés la suavitat dels angles. Si posemsidesigual a 2 obtenim una línia horitzontal. Podem simular una circumferència posant un nombre prou gran de costats, com 99.shape(5, 0.3, 0.01).out() -
gradient( speed = 0): crea un gradient de color.speedés la velocitat de canvi. El gradient inicial té verd a la part inferior i vermell a la part dreta. A la cantonada inferior dreta obtenim groc (la combinació de vermell i verd), i a la cantonada superior esquerra, on no arriba cap dels dos colors, veiem negre. Si assignem una velocitat, la component blava de tots els punts anirà canviant amb el temps.gradient(0.1).out() -
solid( r = 0, g = 0, b = 0, a = 1 ): crea un color sòlid.R,GiBsón els components de color vermell, verd i blau, respectivament, iAés la transparència o canal alfa (1 és completament opac, i 0 completament transparent). Tots els valors van de 0 a 1.solid(0.6, 0.2, 0.8).out() -
src( tex ): permet utilitzar una textura com a font de colors.Texés la textura que volem utilitzar. Aquesta textura pot ser una imatge o un altre patró generat per Hydra. Estudiarem aquestes possibilitats amb detall més endavant.
Seqüenciació
Hem vist que algunes funcions tenen paràmetres que ja ens permeten generar patrons dinàmics. Però Hydra ens ofereix un sistema que ens permet crear seqüeǹcies de valors que es van repetint en el temps, i que es poden utilitzar en el lloc de qualsevol paràmetre.
Ho veurem clar amb un exemple:
shape([3,8,5,4]).out()
Podem veure com la quantitat de costats del polígon va canviant, seguint la seqüència que li hem passat com a paràmetre. La seqüència l’hem escrita com un array: una llista d’elements separats per comes i delimitada per claudàtors.
Podem fer el mateix amb qualsevol altre paràmetre:
voronoi(8, 0.3, [5,3,1,10]).out()
Els nostres arrays també accepten algunes transformacions:
-
fast( speed ): accelera la seqüència (o la frena sispeedés menor a 1).shape([3,8,5,4].fast(4), [.2, 0.4, 0.6].fast(.37)).out() -
smooth(): fa que el canvi entre els valors d’un array sigui gradual en comptes de sobtat:solid([0,1].smooth()).out()
Podem combinar les dues transformacions:
shape([3,8,3,12].fast(.2).smooth()).out()
Funcions transformadores
Hi ha moltes més funcions transformadores que no pas generadores. En veurem només algunes, però pots trobar la llista completa a la documentació oficial de Hydra.
Transformacions geomètriques
Aquestes funcions modifiquen les coordenades dels punts de la textura.
-
rotate( angle = 10, speed = 0 ): gira el patró.angleés l’angle en radiants, ispeedés la velocitat de canvi. -
scale( amount = 1.5, xMult = 1, yMult = 1, offsetX = 0.5, offsetY = 0.5 ): aplica un reescalat a la textura.amountés la proporció d’escalat: 1 manté la mida original, 0.5 la redueix a la meitat i 2 la duplica.xMultiyMultpermeten escalar de forma diferent en horitzontal i vertical.offsetXioffsetYpermeten canviar el centre de l’escalat (0.5 és el centre de la imatge, així que, per defecte, el reescalat es fa centrat). -
repeat( repeatX = 3, repeatY = 3, offsetX = 0, offsetY = 0 ): repeteix la textura els cops que s’indiquin.repeatXirepeatYindiquen la quantitat de repeticions a l’eix X i a l’eix Y.offsetXioffsetYapliquen un desplaçament alternatiu: només afecta una de cada dues repeticions.shape(4).repeat().out()shape(4,.6).rotate(Math.PI/4).repeat(6,6,0.5).out() -
repeatX( repeat = 3, offset = 0 )irepeatY: comrepeat, però només apliquen la repetició en un dels eixos.noise(1).repeatX(9,.5).out() -
scroll( scrollX = 0.5, scrollY = 0.5, speedX = 0, speedY = 0 ): aplica un desplaçament a la textura.scrollXiscrollYés el desplaçament en cada eix.speedXispeedYs’utilitzen per moure dinàmicament la textura.shape().scroll(.2,.3).out()shape().scroll([0,.3,-.4],[.2,0,-.7].fast(1.21)).out()shape(3).scroll(0,0,.2,.2).out() -
scrollX( scroll = 0.5, speed = 0 )iscrollY: apliquen un desplaçament només en un eix. -
kaleid( nSides = 4 ): aplica un efecte de calidoscopi: s’agafa la part de la textura que queda a la cantonada superior esquerra, se centra, i es repeteix en cercle tantes vegades comnSides.noise().kaleid(6).out()osc(4,-.1,1).kaleid(50).out() -
pixelate( pixelX = 20, pixelY = 20 ): crea un efecte de pixelació: s’uneixen molts punts de la textura en un de sol, cosa que redueix la resolució de la textura.pixelXipixelYsón la quantitat de punts resultant en cada eix.noise().pixelate(10,10).out()voronoi(100).pixelate(10,50).kaleid(8).out()
Funcions de transformació de color
Hi ha moltes funcions a Hydra relacionades amb el color, en veurem algunes d’elles. Pots veure’n la llista completa a aquest apartat de la documentació oficial.
-
posterize( bins = 3, gamma = 0.6 ): redueix la quantitat de colors de la textura.binsés el nombre de colors resultants, igammaés un paràmetre que controla la intensitat del color resultant.gradient(0.1).posterize(3, 0.6).out()osc(20,0.2,[0.8,2,5].fast(1.1)) .posterize([2,10].smooth()) .out() -
invert( amount = 1 ): inverteix els colors de la textura.amountés la quantitat d’inversió (1 és totalment invertit, 0 és sense inversió).solid(0.2, 0.5, 0.8).invert([0,1]).out()gradient(0).invert([0,1].smooth()).out() -
color( r = 1, g = 1, b = 1, a = 1 ): aplica un color a la textura.r,gibsón els components de color vermell, verd i blau, respectivament, iaés la transparència (1 és completament opac, i 0 completament transparent). Tots els valors van de 0 a 1.shape(5,.3,.8).color(1,.3,0).out()shape(5,.3,.8) .repeat(5,5) .scroll(0,0,.3,.3) .color([0,1].smooth(), .3, 0) .out() -
colorama( amount = 0.005): aplica un efecte de color automàtic, més intens com més gran siguiamount. El color es converteix internament a l’espai HSV (Hue, Saturation, Value); després, se sumaamounta cadascun dels components, i finalment se’n conserven només els decimals per mantenir-los dins el rang [0, 1].gradient() .colorama([-1,1].fast(.1).smooth()) .out()
Sortides
Hydra, per defecte, té fins a quatre sortides que es poden utilitzar. Això ens permet crear patrons intermitjos, o utilitzar-los com a memòries temporals.
Les quatre sortides s’anomenen o0, o1, o2 i o3. Per defecte, out() envia el resultat a o0, i o0 és la sortida que es visualitza a la pantalla. Podem modificar la sortida d’una expressió indicant-la explícitament a out(), i podem utilitzar render() per mostrar per pantalla qualsevol de les quatre sortides. render() sense cap paràmetre mostrarà les quatre sortides alhora.
osc().out(o0)
voronoi().out(o1)
noise().out(o2)
gradient().out(o3)
render(o0)
Prova a modificar el paràmetre de render() per veure cadascuna de les sortides, o a no passar-li res per veure-les totes alhora.
La funció src() permet fer servir el contingut d’una de les sortides com a textura d’entrada. Per defecte, src() agafa el resultat de o0, però podem especificar quina sortida volem utilitzar.
voronoi().out(o1)
noise().out(o2)
gradient().out(o3)
src(o1).out()
render(o0)
En aquest exemple, prova de modificar el paràmetre de src().
|
|
Barreja de patrons
Disposem de diverses funcions per combinar patrons entre ells.
-
blend( textura, amount = 0.5 ): barreja dues textures. Per cada punt, el resultat és la mitjana ponderada entre el valor de la primera textura i el valor de la segona.amountindica la proporció de cada textura que s’agafa, on 0 correspon només a la primera textura i 1 només a la segona.gradient().out(o0) voronoi().out(o1) src(o0).blend(o1, [0,.25,.5,.75,1]).out(o2) gradient().blend(voronoi(), [0,.25,.5,.75,1]).out(o3) render()En aquest exemple veiem:
-
o0: un gradient. -
o1: el resultat devoronoi(). -
o2: el resultat de barrejar la sortida 0 amb la sortida 1, amb una proporció que va variant entre 0 i 1. -
o3: el resultat de barrejar el gradient amb la textura de Voronoi, amb una proporció que va variant entre 0 i 1.
-
-
add ( textura, amount = 1): similar ablend(), cada punt és la suma de la primera textura sense modificar-se i la proporció de la segona textura indicada aamount.gradient().out(o0) voronoi().out(o1) src(o0).add(o1, [0,.25,.5,.75,1]).out(o2) gradient().add(voronoi(), [0,.25,.5,.75,1]).out(o3) render()És el mateix exemple d’abans, canviant
blend()peradd(). Noteu com aquí el gradient sempre és visible, i hi afegim el patró de Voronoi per sobre com si fos una capa lluminosa. -
diff ( textura ): substrau la segona textura de la primera, amb la particularitat que si algun nombre queda per sota de 0, es resta d'1 per obtenir sempre nombres entre 0 i 1. Per exemple, si el color del primer punt és(0.4, 1, 0.8)i el color del segon punt és(0.6, 0.2, 0.4), el resultat serà(0.8, 0.8, 0.4). El primer 0.8 surt de 0.4-0.6 = -0.2, i com que és negatiu, 1-0.2=0.8.Aquesta manera de calcular evita que el resultat sigui massa fosc, i permet generar colors vius a partir de la diferència entre textures.
gradient().out(o0) voronoi().out(o1) src(o0).diff(o1).out(o2) gradient().diff(voronoi()).out(o3) render() -
layer ( textura ): és similar ablend(), però utilitza el canal alfa de la segona textura per determinar quina part d’aquesta s’ha de superposar sobre la primera.gradient().out(o0) voronoi().color(1,1,1,[0,.25,.5,.75,.1]).out(o1) src(o0).layer(o1).out(o2) gradient().layer( voronoi().color(1,1,1,[0,.25,.5,.75,.1]) ).out(o3) render() -
mask ( textura ): aplica una màscara a la primera textura, basada en la segona: si un punt és blanc, es mostra la primera textura tal qual; si és negre, es veu el negre; si és gris, s’atenua. En cas que la segona textura tingui color, es fa un càlcul previ de la lluminositat total per determinar el nivell de màscara a aplicar.gradient().out(o0) voronoi().out(o1) src(o0).mask(o1).out(o2) gradient().mask(voronoi()).out(o3) render()
Feedback
Ara que ja sabem com combinar textures, podem utilitzar la sortida d’un patró com a una de les textures d’entrada. Dit d’una altra manera, podem utilitzar el resultat d’un frame per construir el següent frame. Aquesta tècnica es coneix com a feedback o retroalimentació.
|
Un frame és la imatge que es mostra en pantalla en un instant donat. Els videos i animacions són una successió de frames que es mostren a una velocitat determinada, anomenada frame rate i que es mesura en frames per segon (FPS). |
Les possibilitats artístiques del feedback són infinites, especialment si ho combinem amb les funcions de modulació que veurem al següent apartat.
Veiem algun exemple d’ús del feedback amb funcions que hem vist fins ara:
shape(3,.3,.8)
.repeat(3)
.diff(
gradient(.6)
).diff(
src(o0)
.rotate([.04,-.04].smooth().fast(.3))
, .9
)
.out()
shape(99,.5,.9)
.blend(src(o0).repeat(4), 1)
.diff(
shape(240,.5,0)
.scrollX(.05)
.rotate(0,.3)
.color([.1,.9].smooth().fast(.2),0,.7)
).diff(
shape(99,.4,.002)
.scrollY(.1)
.rotate(0,-.2)
.color(.6,0,[.1,.8].smooth().fast(.31)
)
)
.out()
|
En contextos visuals, el feedback pot generar patrons fractals, moviments espirals o formes orgàniques que evolucionen de manera autònoma. Jugar amb la rotació, la translació o l’escala de la textura de feedback pot donar lloc a composicions molt expressives. |
Modulació
La modulació consisteix en modificar la geometria d’un patró utilitzant la informació continguda en un patró diferent. Per exemple, en comptes d’aplicar una rotació constant, podem fer que la rotació a cada punt depengui de la intensitat del punt corresponent d’un altre patró.
Hi ha moltes funcions de modulació a Hydra. Aquí només en veurem algunes, però pots experimentar amb qualsevol d’elles.
-
modulate ( textura, amount = 0.1 ): desplaça cada punt de la textura original depenent del color de la segona textura. Específicament, la component vermella de la segona textura determina el desplaçament horitzontal, i la verda, el vertical.amountés la quantitat de desplaçament que s’aplica.osc(20).out(o0) voronoi(5,.5,1.5).out(o1) src(o0).modulateRotate(o1).out(o2) osc(20).modulateRotate(voronoi(5,.5,1.5)).out(o3) render()osc(5) .blend(gradient(.3)) .modulate( src(o0) .colorama( [0,1].smooth().fast(.1) ) .rotate(.1),.5 ).out() -
modulateRotate ( texture, multiple = 1, offset = 0 ): comrotate, però la rotació de cada punt depèn del color de la segona textura (en particular, de la seva component vermella).multipleés la quantitat de rotació que s’aplica, ioffsetés una quantitat fixa de rotació que s’afegeix al resultat.osc(20).out(o0) voronoi(5,.5,1.5).out(o1) src(o0).modulateRotate(o1).out(o2) osc(20).modulateRotate(voronoi(5,.5,1.5)).out(o3) render()src(o0) .colorama(.1) .modulateRotate(src(o1),2) .layer( src(o1).diff(gradient(1))) .out(o0) noise(8,.2) .rotate(.2) .diff(src(o0) .kaleid(3)) .out(o1) -
modulateScale( texture, multiple = 1, offset = 0 ): funciona commodulateRotate(), però modifica l’escalat de cada punt en comptes de la rotacióshape(5,.6,.1).out(o0) voronoi(5,.5,1.5).out(o1) src(o0).modulateRotate(o1).out(o2) shape(5,.6,.1).modulateRotate(voronoi(5,.5,1.5)).out(o3) render()shape(5,.2,.4) .repeat(10) .blend(o0) .kaleid(6) .modulateScale(o1,.2) .out() src(o0) .pixelate(50,50) .rotate(0,.02) .out(o1)
Fonts externes
A més de les funcions generadores que hem vist al principi, Hydra pot fer servir altres fonts com a textures d’entrada. Podem utilitzar la imatge d’una webcam, vídeos, o imatges externes.
Disposem de quatre entrades externes que podem configurar i utilitzar: s0, s1, s2 i s3. Cadascuna pot assignar-se a un recurs extern.
Càmeres
Per utilitzar una càmera web, hem d’inicialitzar-la amb la funció initCam(). Per exemple, s0.initCam() assignarà el flux de la càmera a s0. Després, el podem utilitzar com hem fet abans amb les sortides: src(s0).
Si disposem de més d’una càmera, podem indicar quina volem utilitzar amb un nombre: initCam(2).
|
Encara que parlem de webcam perquè és el cas més comú, es pot utilitzar qualsevol dispositiu físic o virtual que generi un flux continu d’imatge. Per exemple, un microscopi USB, o una càmera virtual configurada a OBS. |
s0.initCam()
src(s0).colorama(.5).diff(noise()).out()
Imatges i videos
Podem utilitzar imatges i vídeos externs com a textures d’entrada. Per fer-ho, hem d’inicialitzar la textura amb initImage() o initVideo(), respectivament, indicant l’adreça del recurs.
|
Per motius de seguretat, el navegador web no pot accedir directament als fitxers del teu ordinador. Tot i que aquest problema es pot solucionar de diverses maneres, la seva relativa complexitat tècnica fa que no les tractem en aquest curs. |
|
Molts serveis d’ús habitual per allotjar vídeos (com YouTube o Vimeo) no permeten accedir al seu contingut des d’altres pàgines web. |
Un servei que sí que permet utilitzar els seus vídeos i imatges és Wikimedia Commons. Per fer-ho, necessitem l’adreça directa del fitxer que volem utilitzar:
s0.initImage("https://upload.wikimedia.org/wikipedia/commons/6/6b/Bronze-winged_jacana_%28Metopidius_indicus%29.jpg")
src(s0)
.modulateRotate(osc(3))
.posterize(8)
.invert(.8)
.out()
s0.initVideo("https://upload.wikimedia.org/wikipedia/commons/1/1c/Lightning_strikes_in_slow_motion_%28240fps%29_in_Muurame%2C_Finland.webm")
src(s0)
.scroll(.5,.3)
.kaleid(5)
.add(o0,.7)
.out()