Files
agent-recur/docs/conversion-guide.ca.md
2026-09-09 17:33:05 +02:00

32 KiB
Raw Permalink Blame History

doc, rev, canonical_language, canonical_source, translations
doc rev canonical_language canonical_source translations
SHADERTOY-RECUR-GUIDE 03 en docs/conversion-guide.en.md
ca
docs/conversion-guide.ca.md (rev 03, al dia)

De Shadertoy a r_e_c_u_r

Shadertoy corre sobre WebGL2, és a dir GLSL ES 3.00. r_e_c_u_r corre sobre OpenGL ES 2.0, és a dir GLSL ES 1.00. No és un canvi de noms: és un llenguatge anterior al qual li falten coses que mig Shadertoy fa servir sense pensar. D'aquí les dues parts de sota: la traducció mecànica (ràpida) i allò que s'ha de reescriure de debò.

Per saber quins uniforms existeixen i per què, vegeu la referència.

Si el teu shader és 1-input, llegeix abans el tutorial de l'autor. tutorial_converting_simple_1input_shader_from_shadertoy és el pas a pas del mateix cyberboy666 i el camí més curt per a un shader que processa el vídeo d'entrada. Aquesta guia és més ampla —cobreix 0-input, els límits de la GPU i quatre errors mesurats a l'aparell— però no el substitueix. Vegeu El tutorial oficial per saber què ensenya i on s'acaba el seu abast.

Part 1 — la traducció mecànica

Estructura

Shadertoy et dona només el cos de mainImage. r_e_c_u_r vol un fitxer complet:

// SHADERTOY
void mainImage(out vec4 fragColor, in vec2 fragCoord){
    vec2 uv = fragCoord/iResolution.xy;
    fragColor = vec4(uv, 0.5 + 0.5*sin(iTime), 1.0);
}
// R_E_C_U_R
//0-input
#ifdef GL_ES
precision mediump float;
#endif

varying vec2 v_texcoord;
uniform vec2 u_resolution;
uniform float u_time;
uniform float u_x0;
uniform float u_x1;
uniform float u_x2;
uniform float u_x3;

void main(){
    vec2 uv = v_texcoord;
    gl_FragColor = vec4(uv, 0.5 + 0.5*sin(u_time), 1.0);
}

La marca //N-input de la primera línia és el que r_e_c_u_r busca per etiquetar el shader al seu menú. Sense ella, el shader apareix com a tipus -.

Taula d'equivalències

Shadertoy r_e_c_u_r Compte amb
void mainImage(out vec4 fragColor, in vec2 fragCoord) void main()
fragColor gl_FragColor
fragCoord v_texcoord * u_resolution O gl_FragCoord.xy, que també existeix
fragCoord/iResolution.xy v_texcoord El cas comú: se substitueix sencer
iResolution u_resolution Shadertoy dona un vec3; aquí és vec2. iResolution.z no existeix
iTime u_time Pot anar enrere i ser negatiu
iChannel0 / iChannel1 u_tex0 / u_tex1
texture(ch, uv) texture2D(ch, uv)
iMouse.xy/iResolution.xy vec2(u_x0, u_x1) Ja ve en 01, no cal dividir
iMouse.z > 0.0 (botó) u_x2 > 0.5 Els params són continus; es fan servir com a interruptor
iTimeDelta, iFrame, iDate, iSampleRate, iChannelResolution no existeixen Treure'ls o simular-los

Els paràmetres: de constants màgiques a comandaments

Aquesta és la part que fa que un port valgui la pena. Un shader de Shadertoy té números fixos per tot arreu; a r_e_c_u_r aquells números poden ser els comandaments.

u_x0u_x3 arriben sempre entre 0.0 i 1.0 (conjur.cpp:54). L'escalat es fa dins del shader:

float velocitat = u_x0 * 4.0;           // 0 → 4
float zoom      = 0.5 + u_x1 * 3.5;     // 0.5 → 4 (mai 0, que rebenta una divisio)
float angle     = u_x2 * 6.2831853;     // volta completa
bool  invertir  = u_x3 > 0.5;           // interruptor

Triar quins quatre números mereixen comandament és la decisió de disseny del port. Dues regles que funcionen bé:

  • Amb els quatre comandaments a zero, el shader s'hauria de veure com l'original. És com arriba en carregar-lo a l'aparell: si a zero es veu pla o negre, sembla trencat. Compte amb què vol dir «com l'original»: assemblar-s'hi, no deixar els números a zero. Són coses diferents, i el 2026-09-06 va costar tres rondes de «segueix sense encertar». En el port d'un capvespre, el comandament de color a zero deixava la paleta sense desplaçar, i això dona un cel blau de dia; l'original té el sol baix i el cel calent, que apareix amb el comandament entre 0.20 i 0.30. El port era fidel al codi i no s'assemblava a l'original. S'arregla amb una constant que mou el repòs on hi ha l'aspecte bo, i el comandament gira des d'allà: costa zero operacions. Escombra cada comandament de punta a punta i MIRA abans de decidir on va el zero.
  • Cada comandament ha de canviar alguna cosa reconeixible a tres metres, no afinar un decimal. Si l'original cicla automàticament entre estats, poder triar l'estat sol ser el comandament més valuós de tots: sense ell, toca esperar.

A més: si l'ajust X3_AS_SPEED està activat, u_x3 no arriba mai al shader; passa a ser el control de velocitat. Un shader que necessiti els quatre params només funciona amb aquell ajust desactivat. Dissenya per a tres.

Proporció i centrat

Shadertoy sol normalitzar així perquè els cercles surtin rodons:

vec2 uv = (fragCoord - 0.5*iResolution.xy)/iResolution.y;

v_texcoord va de 0 a 1 en tots dos eixos, així que copiar-ho tal qual deixa tot estirat en una sortida panoràmica. L'equivalent:

vec2 uv = v_texcoord - 0.5;
uv.x *= u_resolution.x / u_resolution.y;

Part 2 — allò que GLSL ES 1.00 no té

Aquí és on se'n van les hores. Si el shader de Shadertoy fa servir res d'aquesta llista, no és copiar i enganxar: s'ha de reescriure.

Operadors de bits — el que més trenca

GLSL ES 1.00 no té &, |, ^, <<, >> ni %. Els hashos moderns de Shadertoy els fan servir constantment:

// SHADERTOY — no compila a la Pi
uint h = uint(p.x) * 374761393u;
h = (h ^ (h >> 13u)) * 1274126177u;

Substituir per un hash de coma flotant dels de tota la vida:

float hash(vec2 p){
    return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

No produeix el mateix soroll, així que el resultat no serà idèntic a l'original. És una reescriptura, no una traducció, i convé dir-ho en veu alta.

Tampoc no hi ha uint, ni uvec2/3/4, ni %. Per al mòdul: mod(a, b) amb floats.

Bucles

Els límits han de ser constants en temps de compilació. Això no compila:

for(int i = 0; i < passos; i++){ ... }        // `passos` es una variable

Es posa el màxim com a constant i se surt amb un break:

const int MAX_PASSOS = 64;
for(int i = 0; i < MAX_PASSOS; i++){
    if(float(i) >= passos) break;
    ...
}

Tampoc no hi ha while, ni switch, ni indexar arrays amb un índex variable.

Funcions que falten

No hi és a ES 1.00 Alternativa
textureLod, texelFetch texture2D a seques (sense control de nivell de mipmap)
dFdx, dFdy, fwidth No existeixen en aquest aparell: mesurat el 2026-09-09, l'extensió GL_OES_standard_derivatives no hi és. Calcular l'amplada a mà (píxel del costat, geometria, o analíticament si el mapa és afí)
round, trunc, roundEven floor(x + 0.5), floor(abs(x))*sign(x)
sinh, cosh, tanh Escriure-les amb exp()
inverse, transpose, determinant A mà, o redissenyar per no necessitar-les
isnan, isinf No hi ha manera neta
mix() amb booleans Només floats

Noms que tapen funcions

Una variable local anomenada com una funció incorporada (mix, step, length…) tapa aquella funció dins del seu àmbit. Compila a escriptori i és imprevisible a la Pi. Reanomena-la.

El bucle d'antialiasing multiplica la mida per AA*AA

Un parany que se suma al de la mida, i facil de passar per alt. Un bucle aixi:

#define AA 2.
for(float i = 0.; i < AA*AA - 0.5; i += 1.) {
    ...
    vec3 c = el_meu_patro(p);
    ...
}

te limits constants, aixi que el compilador el desenrotlla. El cos sencer -- funcio de patro inclosa -- acaba al codi final quatre vegades. Amb AA a 3 en serien nou.

Mesurat a la maquina: de cinc ports que nomes es diferencien en la funcio de patro, els quatre amb un patro de 10-15 linies van funcionar, i el que tenia 31 linies i 8 condicions va sortir negre. Abaixant-lo a AA 1. hi va entrar.

Aixi que quan un port es a prop del limit, AA es la palanca mes barata: divideix per quatre el codi generat canviant un caracter. El que es perd es el suavitzat de vores.

Abans de tocar AA, busca condicions repetides. En aquell mateix patro hi havia tres branques que tornaven a comprovar el mateix rang; treure-ho fora va llevar sis comparacions per mostra -- vint-i-quatre ja desenrotllat -- sense canviar ni un sol pixel.

Quan el soroll ES l'efecte

Hi ha shaders que no calculen una imatge: calculen un accident controlat. El senyal es una constant absurda. time *= 5e19 no es un compte, es llencar la precisio a proposit perque el mod() de despres llegeixi bits que ja no signifiquen res. La sortida es soroll numeric, volent.

Dues consequencies, i totes dues convé dir-les en veu alta abans de portar:

  • Necessita highp. 5e19 en mediump es infinit, i no queda shader.
  • No es veura igual que al navegador, mai, perque depen de com arrodoneixi cada GPU. Prometre un resultat identic es prometre el que no es pot complir.

El mateix va passar al reves amb un shader de reixa el clapejat del qual venia de la precisio baixa del navegador: l'aparell el dibuixava net i el port semblava malament per estar be. En tots dos casos la regla es la mateixa: quan un render i la seva referencia no coincideixen, cal preguntar-se si la referencia esta ensenyant un artefacte.

check_port.py avisa de qualsevol literal per sobre de 65504 justament per aixo.

Un macro amb el parametre sense parentesis NO s'«arregla»

Mesurat el 2026-09-05 portant el shader de Noztol. Defineix la seva paleta aixi:

#define p(t, a, b, c, d) ( a + b*cos( 6.28318*(c*t+d) ) )

t va pelat. Aixo es inofensiu fins que es crida el macro amb una suma, que es el que fa: sp(sin(x) + terme). En expandir queda c*sin(x) + terme, o sia que el segon sumand no va multiplicat per c. Si li poses els parentesis «bons» al voltant de (t), la imatge canvia: el port se'n va separar 36/255 en el 72% dels pixels, i va tornar a 0/255 tan bon punt es van treure.

Mateix principi que el soroll de precisio a proposit: l'artefacte es el shader. En portar, expandir el macro a ma amb la raresa inclosa i dir en un comentari que es volent, o el seguent que ho llegeixi ho «arregla» un altre cop.

Es tambe la rao per expandir el macro: el de dalt es diu p, i l'original te tres funcions el parametre de les quals tambe es diu p. No va arribar a disparar mai, perque un macro amb parametres nomes s'expandeix davant d'un (, pero es un cep parat.

L'eix Y d'aquest aparell va al reves que el d'un escriptori

Mesurat el 2026-09-05 carregant atardecer a la Pi: gl_FragCoord.y = 0 cau A DALT de la pantalla. Un paisatge escrit com a Shadertoy surt de cap per avall, amb el terra per sobre del cel. Es corregeix en una linia:

vec2 g = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);

Els ports anteriors no ho van caçar perque cap no te dalt i baix: anells, reixes, una ona centrada. Va caldre un paisatge per veure-ho.

I hi ha un detall que fa perdre temps: la captura PNG de glslViewer coincideix amb l'aparell, perque tambe posa gl_FragCoord.y = 0 a la fila 0 del fitxer, o sia a dalt. Es al reves del que un espera d'un PNG, aixi que la temptacio es capgirar-la per «veure-la be» -- i aleshores la captura deixa d'assemblar-se a la pantalla justament quan mes falta fa. No la capgiris. El que surt de glslViewer es el que veuras.

El raymarching no hi cap --- mesurat, no suposat

Un shader que marxa raigs no entra en aquest aparell, i la manera de saber-ho no es intentar-ho. Mesurat el 2026-09-05 amb un shader de keim (MtGSWc), l'original del qual costa 234 crides a map() per pixel:

Versio Crides a map() A l'aparell
Original 234 impossible
Reduida 19 s'encalla, les figures perden definicio
Mes reduida 13 tambe s'encalla
Redissenyada en 2D 1 avaluacio funciona

Aquell map() portava a dins un exp, un sin, dos mod i quatre length. Per comparar: un bucle de 8 voltes amb un cos molt mes barat va a 13.2 fps.

Aixi que la maniobra no es abaixar els passos. No es questio d'afinar: no hi cap amb cap nombre de passos que valgui la pena. Cal redissenyar la idea en 2D --una avaluacio per pixel, sense marxa-- que es el que va funcionar aqui i abans amb un Mandelbox. I conve dir-ho aviat, abans de gastar la tarda en una traduccio que no pot correr.

check_port.py ja estima les operacions per pixel i avisa per sobre de 250. El llindar esta calibrat amb shaders reals d'aquest aparell: els que van arriben a 166 i el que s'encalla n'estima 360.

Si es massa gran no dibuixa res, i s'ha de partir

Mesurat a la maquina el 2026-09-03. Un shader massa gran compila, enllaca i despres no dibuixa: pantalla negra o el fotograma anterior congelat, sense cap error enlloc -- ni al registre de GLSL ni a la interficie de r_e_c_u_r.

Dos punts mesurats, un a cada costat del limit, del mateix shader:

linies de codi funcions operacions A la Pi
Cinc funcions de patro 146 13 69 pantalla negra
Dues funcions de patro 85 10 38 funciona

Per comparar, cap dels 146 shaders que porta la imatge de r_e_c_u_r passa de 90 linies ni de 23 operacions. Tots porten un efecte per fitxer, i aixo no es una decisio estetica: es el que hi cap.

Aixi que quan un shader de Shadertoy fica diversos efectes diferents en un fitxer darrere d'un selector -- cosa molt habitual -- el port son diversos shaders, no un. Es perd el selector intern i es guanya un comandament, perque r_e_c_u_r ja deixa canviar de shader des de la interficie.

check_port.py avisa per sobre de 100 linies o 45 operacions.

Dins d'un bucle, assigna des d'una funcio nomes a la declaracio

Comprovat a la maquina el 2026-09-03, despres d'una bisecció llarga.

Dins del cos d'un bucle, aixo genera codi incorrecte al compilador GLSL de la Pi:

vec3 c;
c = el_meu_patro(p);      // assignada des d'una funcio propia

El shader compila sense ni un sol avis i s'executa, pero la sortida deixa de dependre d'u_time i dels comandaments: surt una imatge congelada, o negra. Ni el registre diu res, ni la interficie.

Inicialitza-la a la mateixa declaracio:

vec3 c = el_meu_patro(p);

Donar-li un valor per defecte a la declaracio no basta: el problema es la reassignacio, no que falti l'inicialitzador. Si has de triar entre diverses funcions, mou l'eleccio a una funcio a part perque el cos del bucle tingui exactament una assignacio:

vec3 tria(int quin, vec2 p) {
    if (quin == 0) return patro_a(p);
    if (quin == 1) return patro_b(p);
    return patro_c(p);
}
...
vec3 c = tria(quin, p);

Les tres parts de la regla compten, i cadascuna es va provar per separat:

Dins d'un bucle Resultat a la maquina
vec3 c = la_meva_funcio(p); funciona
vec3 c; c = vec3(1.0, 0.0, 0.0); (expressio en linia) funciona
vec3 c; c = la_meva_funcio(p); congelat / negre
vec3 c = vec3(0.0); c = la_meva_funcio(p); congelat / negre

Fora d'un bucle, declarar sense inicialitzar no dona problema: 22 dels 124 shaders que porta la imatge de r_e_c_u_r ho fan. Cap ho fa dins d'un bucle.

check_port.py detecta exactament aquella combinacio: 0 falsos positius sobre els 141 shaders que sabem que funcionen en un aparell real, i caça els 5 nostres que fallaven.

Aquesta va costar dies, i val la pena entendre per que. Totes les senyals apuntaven en la direccio equivocada: compila, s'executa, la interficie mostra el shader com si estigues funcionant, i el codi es GLSL absolutament corrent que funciona en qualsevol escriptori. L'unica manera de trobar-ho va ser bisecar de canvi en canvi contra un shader que se sabia bo en aquell aparell concret.

Un shader 0-input no pot fer servir v_texcoord

Trobat a la maquina el 2026-09-02, despres de dies al darrere. Es el parany mes car de tots els que ens han sortit.

Mira quina geometria dibuixa conjur (ofxVideoArtTools/src/conjur.cpp, apply()):

if(textures.size() > 0 ){
    textures[0].draw(0, 0, ofGetWidth(), ofGetHeight());   // genera texcoords
} else {
    ofDrawRectangle(0, 0, ofGetWidth(), ofGetHeight());    // NO les genera
}

Sense textures d'entrada, la geometria es un ofDrawRectangle, que no porta coordenades de textura. L'atribut texcoord de default.vert queda indefinit, aixi que tant tcoord com v_texcoord arriben al fragment shader constants. Tots els pixels calculen el mateix i surt una pantalla d'un color pla, sense cap missatge d'error.

Cal treure la coordenada de gl_FragCoord:

vec2 uv = gl_FragCoord.xy / u_resolution.xy;

Els shaders del repositori original segueixen aquesta regla sense haver-la escrit mai. Comptat sobre un aparell real:

fa servir v_texcoord fa servir gl_FragCoord
Els 5 0-input del repositori 0 5
Els 12 1-input del repositori 12 0

Disset de disset, sense excepcions. check_port.py ja ho detecta.

Per que va costar tant trobar-ho: la fallada depen de l'estat en execucio, no del fitxer. Si hi ha un video reproduint-se, textures no esta buit, la geometria es un quadrilater amb textura i v_texcoord funciona perfectament — el mateix shader es comporta be. Per aixo el mateix fitxer semblava funcionar un dia i no l'endema, i per aixo el "color de la fallada" anava canviant: el que es veu es el que hi hagi a sota en aquell moment.

El fitxer ha de ser ASCII pur

Descobert per les males el 2026-08-29: dos shaders que renderitzaven perfectament a glslViewer donaven pantalla verda a l'aparell, sense cap missatge d'error. L'única cosa que tenien en comú eren comentaris amb accents, guions llargs i cometes tipogràfiques.

Els 23 shaders que porta r_e_c_u_r són ASCII pur. Ni un accent entre tots ells. El controlador de la Pi és més estricte que el d'un escriptori i rebutja els bytes no-ASCII fins i tot dins de comentaris, on un controlador d'escriptori simplement els ignora.

Escriu els comentaris en ASCII. check_port.py ho detecta.

Aquest és especialment dolent perquè totes les senyals apunten en la direcció equivocada: compila al portàtil, el revisor el donava per bo, i r_e_c_u_r el mostra com si estigués corrent.

r_e_c_u_r no et dirà que el shader ha fallat

video_centre/shaders.py:18 documenta un estat '!' que significa error, però no hi ha ni una sola línia del codi que l'assigni. Un shader que no compila es mostra exactament igual que un que funciona.

Per veure l'error real de GLSL cal la sortida estàndard de c_o_n_j_u_r. Es llança amb subprocess.Popen sense redirecció (actions.py:573), així que la seva sortida va on vagi la consola de r_e_c_u_r. Per SSH:

pkill c_o_n_j_u_r
/home/pi/openframeworks10.1/apps/myApps/c_o_n_j_u_r/bin/c_o_n_j_u_r

Després es carrega el shader des de la interfície i es llegeix el terminal. openFrameworks imprimeix el registre complet de GLSL, amb el número de línia de la fallada.

Multipassada: el límit gran

Shadertoy permet Buffer A/B/C/D: passades que llegeixen el seu propi fotograma anterior. Tot el feedback, els autòmats cel·lulars, els fluids i les simulacions depenen d'això. No hi ha equivalent directe en un shader de r_e_c_u_r.

El que sí que hi ha, i dona molt de joc, és diferent:

  • Les 3 capes de shader. S'apliquen en cadena, i la capa 1 rep a u_tex0 la sortida de la capa 0 i a u_tex1 l'entrada de la capa 0 (ofApp.cpp:115-129). Són diverses passades, però sense memòria del fotograma anterior.
  • El mode detour de r_e_c_u_r, que sí que és un bucle de realimentació de vídeo. L'ajust SHADER_POSITION decideix si els shaders van abans o després.

A la pràctica: un shader de Shadertoy amb Buffers no es porta. O es redissenya la idea al voltant de detour, o es descarta. Digues-ho aviat, abans de gastar una hora traduint una cosa que no pot funcionar.

Precisió

precision mediump float; és el que porten tots els shaders del repositori. En mediump el rang útil és curt i u_time creix sense límit: al cap de pocs minuts apareixen bandes i salts en qualsevol cosa que faci sin(u_time * alguna_cosa_gran).

Dos remeis, per ordre:

  1. Embolcallar el temps. Si el shader té un cicle natural, mod(u_time, durada_del_cicle) dona exactament la mateixa seqüència sense créixer. Si no, mod(u_time, 100.0) sol ser prou.
  2. Demanar més precisió sense trencar la compatibilitat:
#ifdef GL_ES
  #ifdef GL_FRAGMENT_PRECISION_HIGH
    precision highp float;
  #else
    precision mediump float;
  #endif
#endif

[PER VERIFICAR A LA MÀQUINA] si el highp en fragment shader està disponible i què costa.

Rendiment

És una Raspberry Pi renderitzant vídeo alhora. Un raymarching de 128 passos que va suau al navegador aquí va a batzegades. El que més costa, per ordre: bucles d'antialiasing (el cost va al quadrat — AA 4. són 16 mostres per píxel), raymarching amb molts passos, i pow/log/atan dins de bucles.

Abaixa aquests números i deixa un comentari dient com pujar-los, per poder ajustar el shader a la màquina sense rellegir-lo sencer. Val més un que corre a 25 fps que un altre més bonic a 4.

Mesurat a l'aparell (2026-09-04), i canvia la manera d'ajustar. El sostre són 30.0 fps, i és un topall de programari, no de la pantalla: conjur fa ofSetFrameRate(30) amb ofSetVerticalSync(false) (ofApp.cpp:11-12). Així que un shader que marca 30.0 és al topall i potser li sobra marge de GPU; per sota de 30 el que es mesura és la màquina. El quadre és de 720x480 perquè /boot/config.txt porta sdtv_mode=16 (NTSC progressiu) i conjur pren la mida de la pantalla. Un shader el bucle del qual feia 8 voltes, amb dos smoothstep+distance per volta, va mesurar:

Voltes del bucle fps Interval entre fotogrames
8 13.2 76 ms
10 6.6 152 ms
12 4.8 208 ms
16 3.9 256 ms

D'aquí en surten dues coses, i cap de les dues es veu des de l'escriptori.

És un graó, no una costa. El cost per volta salta de 9.5 ms amb 8 voltes a 15.2 ms amb 10, i després es queda pla. En créixer el bucle desenrotllat es creua algun llindar de la GPU, i a partir d'aquí el shader sencer surt més car. Així que quan un port va just, «retallar-lo una mica» és la maniobra equivocada: cal esbrinar de quin costat del graó és i posar-se al bo. És el mateix tipus de llindar que el límit de mida que fa que un shader no dibuixi res: primer es posa lent, més enllà desapareix.

Compara l'interval entre fotogrames amb la transició més curta que vulguis veure. Si el shader té una lluïssor, un cop o qualsevol atac breu, mesura quant dura i posa-ho al costat de l'interval. Al shader de dalt la lluïssor durava 111 ms: amb 8 voltes (76 ms) es recull; amb 10 (152 ms) es perd més vegades de les que es veu, i aleshores el port sembla mal fet en comptes de lent. Una transició més curta que l'interval no es veu dèbil: no es veu, i res no ho avisa.

Per tenir aquests números en comptes d'endevinar-los, vegeu l'envoltori d'instrumentacio/: compta fotogrames a l'aparell i els escriu al mateix registre que les càrregues de shader.

I una mesura solta no ordena dos shaders semblants. El mateix shader --sense tocar-ne una linia-- va donar 13.7 fps en una sessio i 10.1 en la seguent: un 35% de diferencia segons que estigues fent l'aparell alhora. Serveix per saber si una cosa hi cap o no, que es una diferencia de tres vegades; no per decidir entre dues versions que es porten un 20%. Per a aixo cal mesurar-les a la mateixa sessio, seguides.

El tutorial oficial, i en què se n'aparta aquesta guia

cyberboy666 en va escriure un: tutorial_converting_simple_1input_shader_from_shadertoy. Si estàs portant un shader 1-input, llegeix-lo. És curt, és del mateix autor i és el que segueix la resta de la comunitat. El que ve ara no el corregeix —tot el que diu quadra amb el codi font— sinó que dibuixa on s'acaba el seu abast.

Està escrit en la convenció antiga (tcoord, tex, tres, fparams, ftime), que ve del mòdul Structure d'Erogenous Tones. Aquella convenció funciona —conjur enllaça els dos jocs de noms a cada fotograma, sense condició—, però dels 22 shaders que porta el mateix repositori de r_e_c_u_r, 20 són moderns, 1 antic i 1 mixt. Aquesta guia fa servir el joc modern perquè els shaders nous s'assemblin a ells.

Tres coses seves que convé endur-se:

  • Provar al sandbox abans que a l'aparell (vegeu Previsualitzar sense la Pi). És el més útil que té, i que no fos a les nostres eines no és casualitat: va costar una sessió sencera de depuració descobrir el forat que tapa.
  • Que el comandament a zero signifiqui «sense efecte». El seu idioma per a un multiplicador és (1.0 + f1) en comptes de f1: en repòs multiplica per un, a fons duplica. Multiplicar pel paràmetre pelat anul·la la imatge en la posició de repòs.
  • Els literals numèrics necessiten punt decimal: 1.0, mai 1.

Quatre llocs on s'acaba el seu abast. No són errors del tutorial, són casos que no tracta, i cadascun d'ells dona pantalla plana o congelada sense cap missatge d'error:

  • La seva idea central —«tcoord ja ve normalitzada, no divideixis per la resolució»— val només mentre s'estigui dibuixant una textura. En un shader generatiu sense vídeo carregat, conjur::apply() cau a ofDrawRectangle(), que no genera coordenades de textura, i les dues varyings arriben constants. Per a 0-input, gl_FragCoord.xy / u_resolution. Vegeu Un shader 0-input no pot fer servir v_texcoord.
  • La seva plantilla declara iparams com a «4 ints coming in». Està mort: fixat a (0,0,0,0).
  • El seu float time = float(itime) + ftime; no és un rellotge continu, perquè itime és ceil() i no floor(). Fes servir u_time.
  • El seu shader final no porta marca //N-input, i shaders.py sí que la llegeix.

Les seves marques //f0:: són metadades del sandbox, no de r_e_c_u_r: determine_shader_parameter_number retorna 4 sense mirar res.

Part 3 — el procediment

  1. Mirar la llicència primer. Shadertoy publica sota CC BY-NC-SA 3.0 per defecte, llevat que l'autor digui una altra cosa al mateix codi. La clàusula NC vol dir que no es pot fer servir en un bolo pagat ni en res comercial. I compte: r_e_c_u_r és GPL-3.0, amb la qual NC no és compatible, així que un port derivat de material NC no es pot aportar de tornada al projecte.

  2. Guardar l'original amb una capçalera d'atribució (autor, URL, llicència, data de consulta). Si no tens l'URL, escriu [PENDENT] en comptes d'inventar-te-la: els identificadors de Shadertoy són opacs i un enllaç endevinat apunta a un altre shader.

  3. Traduir el mecànic amb la taula de la part 1.

  4. Passar el revisor, que fa aquella feina per tu:

    ./check_port.py el_meu_shader.frag
    

    Marca [BREAKS] allò que no compila en GLSL ES 1.00, [MISSING] els requisits de r_e_c_u_r que no hi són i [CHECK] allò que depèn de la màquina. No és un compilador: que surti net no garanteix que compili a la Pi, però que surti brut garanteix que no. (Els seus missatges són en anglès; amb --lang es surten en castellà. Encara no parla català.)

  5. Triar els params. Quins tres o quatre números es converteixen en comandaments.

  6. Previsualitzar en un escriptori abans de tocar la Pi (vegeu més avall).

  7. Guardar a shaders/N-input/ amb la marca correcta.

  8. Copiar a la màquina i provar de debò.

Previsualitzar sense la Pi

Dues eines, i cacen coses diferents. Fes servir totes dues.

El sandbox, per a compatibilitat. https://glsl.erogenous-tones.comclick to create new shader, hi enganxes i compila a mesura que escrius. És WebGL 1, que és el mateix perfil GLES 2.0 que corre la Pi, així que rebutja allò que rebutjaria l'aparell. Això és justament el que no pot fer la previsualització d'escriptori de sota. Compte: porta la plantilla antiga, així que un shader escrit amb u_x0/u_resolution necessita que li adaptis els uniforms —o que els declaris com a variables soltes— per compilar-hi.

No veu el límit de mida ni pot reproduir els paranys que depenen de l'estat. Tanca el forat de sintaxi, no el de l'aparell.

glslViewer, per iterar sobre la forma. L'autor el recomana, del mateix que va escriure The Book of Shaders, i diu que els uniforms de conjur hi són compatibles (notes_on_shader_formats.md). Edites, guardes i la finestra s'actualitza sola.

glslViewer shaders/0-input/el_meu_shader.frag -l

Amb textura d'entrada, per als d'1 i 2 inputs:

glslViewer shaders/1-input/el_meu_shader.frag una_imatge.png -l

Per moure un paràmetre, s'escriu a la consola de glslViewer mentre corre:

u_x0,0.35

Per automatitzar —útil per generar captures de diversos shaders de cop— hi ha --headless i -E, que executa ordres i surt:

glslViewer el_meu_shader.frag --headless -e "u_x0,0.6" -E "screenshot,sortida.png"

Comparar el port amb l'original, amb numeros

Renderitzar tots dos i mirar-los no basta per als canvis que importen. Treu PNG dels dos i resta'ls canal a canal, pero clava abans el temps dins del shader:

sed -e 's/^uniform float u_time;/const float u_time = 3.0;/' \
    -e 's/^uniform float u_x\([0-3]\);/const float u_x\1 = 0.0;/' port.frag > fixat.frag
glslViewer fixat.frag --headless -E "screenshot,sortida.png"

El -e "time,3.0" de glslViewer no el clava: el mateix shader renderitzat dues vegades al mateix temps nominal donava fins a 55/255 de diferencia per canal en un instant de canvi rapid, mes que diverses de les diferencies reals que voliem mesurar. Amb el valor compilat a dins, dues passades surten identiques byte a byte i el soroll baixa a zero.

Es el que converteix un "es veu semblant" en un "0/255 de diferencia en tres instants, aixi que l'unica desviacio es la que vaig decidir a proposit".

El parany de glslViewer: ofereix molt més del que hi ha a la màquina

glslViewer dona uns 40 uniforms (vegeu UNIFORMS.md al seu repositori). conjur n'enllaça cinc. Els únics presents a tots dos són u_time, u_resolution, u_tex0 i u_tex1.

Tota la resta funciona a l'escriptori i falla en silenci a la Pi: l'uniform no s'enllaça, arriba a zero, i el shader es veu congelat o pla sense donar cap error. Els que més s'hi colen:

Existeix a glslViewer A r_e_c_u_r
u_mouse No. L'error número u. Fer servir u_x0/u_x1
u_delta, u_frame, u_date No
u_pixelDensity, u_sceneFps, u_sceneMs No
u_view2d (pan/zoom amb el ratolí) No
u_camera*, u_light*, u_*Matrix No

Al revés també: u_x0u_x3 són de conjur, no de glslViewer. Allà es fixen a mà des de la seva consola.

Avís: glslViewer en un escriptori compila amb un GLSL més modern que el de la Pi. Que funcioni allà no garanteix que compili a l'aparell. Serveix per iterar sobre la forma. Per a compatibilitat, el sandbox de dalt; l'última paraula, la màquina.

Copiar a la màquina

r_e_c_u_r llegeix shaders des d'un USB muntat, des de /home/pi/r_e_c_u_r/Shaders i des de /home/pi/Shaders (data.py:34). El menys invasiu és /home/pi/Shaders, que deixa net el repositori de r_e_c_u_r:

rsync -av --dry-run shaders/ pi@IP_DE_LA_TEVA_PI:/home/pi/Shaders/

Treure --dry-run quan la llista de fitxers sigui l'esperada.