GLSL.cheatsheet

Фрагментні шейдери в браузері · book of shaders · iquilezles.org · клік по коду = копія

uniform uv / p math d = sdf(p) fragColor

Точки входу

той самий шейдер, чотири обгортки
Фрагментний шейдер — це функція «піксель → колір», яка виконується для кожного пікселя незалежно. Спільного стану між пікселями немає, циклів «по всій картинці» немає, порядку виконання немає. Усе, що відрізняє одну обгортку від іншої — як називається вхід і куди писати вихід.
WebGL1 · GLSL ES 1.00
gl_FragCoord — координата в пікселях, центр пікселя має дробову .5
precision mediump float; uniform vec2 u_res; uniform float u_time; void main(){ vec2 uv = gl_FragCoord.xy / u_res; gl_FragColor = vec4(uv, 0.5, 1.0); }
WebGL2 · GLSL ES 3.00
#version 300 es мусить бути ПЕРШИМ рядком файлу — навіть коментар вище ламає компіляцію
#version 300 es precision highp float; uniform vec2 u_res; out vec4 fragColor; void main(){ fragColor = vec4(gl_FragCoord.xy/u_res, 0.5, 1.0); }
Shadertoy
пише не в gl_FragColor, а в out-параметр mainImage; сама обгортка прихована
void mainImage(out vec4 O, in vec2 U){ vec2 uv = U / iResolution.xy; O = vec4(uv, 0.5, 1.0); }
touchviz · власна збірка
uniform-набір: u_time, u_res, u_hue, u_speed, u_warp, u_density, u_glow, u_grain, u_trail, u_seed
vec2 p = (2.0*gl_FragCoord.xy - u_res)/u_res.y; float d = length(p) - 0.4*u_density;
Shadertoy uniformтипщо всередині
iResolutionvec3розмір вьюпорта в пікселях; беруть .xy, .z — pixel aspect
iTimefloatсекунди від старту шейдера
iTimeDeltafloatтривалість кадру, секунди
iFrameintномер кадру; зручно для «зробити щось у кадрі 0»
iMousevec4.xy — поточна позиція поки затиснута ЛКМ, .zw — точка кліку (знак zw кодує стан кнопки)
iChannel0..3sampler2Dслоти входу: текстура, буфер, кубмапа, відео, аудіо, клавіатура
iChannelResolution[4]vec3розміри того, що прив'язане до слотів
iDatevec4рік, місяць, день, секунди доби — рандом-сід, що не повторюється
iSampleRatefloatчастота дискретизації звуку (44100)

Типи і свізлінг

половина помилок компіляції — тут
float — крапка обов'язкова
неявного int→float немає: 1 і 1.0 — різні типи
float x = 1.0; // ок float y = 1; // помилка компіляції float z = 2.0/3; // теж помилка
swizzle .xyzw · .rgba · .stpq
набори не змішуються між собою; свізл працює і зліва від =
vec3 c = col.bgr; // перестановка vec3 g = col.rrr; // дублювання col.rg = vec2(1.0, 0.0); // col.rx — помилка: різні набори
конструктори
скаляр розтягується на всі компоненти; вектори склеюються
vec3 white = vec3(1.0); vec4 rgba = vec4(rgb, 1.0); vec2 xy = pos.xy;
mat2 / mat3 — column-major
mat2(a,b,c,d) заповнює СТОВПЦІ, не рядки. Звідси плутанина зі знаком у поворотах
mat2 rot(float a){ float c = cos(a), s = sin(a); return mat2(c, s, -s, c); // CCW для rot(a)*p } // mat2(c,-s,s,c) поверне в інший бік
precision — ES 1.00
у фрагментному шейдері ES 1.00 float без precision не компілюється; highp у фрагменті — опційний
#ifdef GL_FRAGMENT_PRECISION_HIGH precision highp float; #else precision mediump float; #endif
const · uniform · varying/in
uniform однаковий для всіх пікселів кадру; varying (ES1) = in (ES3) інтерполюється між вершинами
const float TAU = 6.28318530718; uniform float u_time; varying vec2 v_uv; // ES 1.00 in vec2 v_uv; // ES 3.00

Координатні простори

99% шейдера — це те, ЯК порахували p
uv 0..1
текстурний простір; розтягується разом із вікном — коло стане еліпсом
vec2 uv = fragCoord / iResolution.xy;
p — центр, аспект по висоті
робочий дефолт: 0 у центрі, −1..1 по вертикалі, ширше по горизонталі. Коло лишається колом
vec2 p = (2.0*fragCoord - iResolution.xy) / iResolution.y;
полярні
atan(y,x) — двоаргументний, дає −π..π з правильним квадрантом
float r = length(p); float a = atan(p.y, p.x);
тайлінг + id
клітинка й номер клітинки: id живить рандом на клітинку
vec2 g = p * 4.0; vec2 id = floor(g); vec2 q = fract(g) - 0.5;
дзеркало / фолд
abs — симетрія по осі; повторний фолд дає ромби й «кристали»
p = abs(p) - 0.3; if (p.x < p.y) p = p.yx;
калейдоскоп на n секторів
згорнути кут у сектор і розгорнути назад у xy
float n = 6.0; float a = atan(p.y, p.x); float r = length(p); a = mod(a, TAU/n) - TAU/(2.0*n); p = vec2(cos(a), sin(a)) * r;

Вбудовані функції

усі покомпонентні: працюють і на float, і на vecN
mix(a, b, t)
лінійна інтерполяція a*(1−t) + b*t; t поза 0..1 не обрізається
vec3 col = mix(bg, fg, 0.3);
clamp(x, lo, hi)
saturate у GLSL немає — це clamp(x, 0.0, 1.0)
float s = clamp(x, 0.0, 1.0);
step(edge, x)
0 або 1, без згладжування — на діагоналях дасть драбинку
float m = step(0.5, uv.x);
smoothstep(e0, e1, x)
згладжений перехід; при e0 >= e1 результат за специфікацією не визначений
float m = smoothstep(0.4, 0.5, r); // зворотний край: smoothstep(0.5, 0.4, r)
fract(x)
x − floor(x); для від'ємних теж додатний — база всіх повторень
float saw = fract(uv.x * 8.0);
mod(x, y)
x − y*floor(x/y), знак наслідує y. Це НЕ % з C: mod(-1.0, 3.0) = 2.0. Оператор % для float у GLSL відсутній узагалі
float t = mod(u_time, 4.0);
length · distance · dot
length(v) = distance(v, 0); dot(v,v) — квадрат довжини без sqrt, дешевше
float r = length(p); float d2 = dot(p, p);
normalize(v)
на нульовому векторі — ділення на нуль і NaN, який потім рознесе всю картинку
vec3 n = normalize(v + 1e-6);
pow(x, y)
не визначено для x < 0 і для x = 0, y <= 0. Перед pow — clamp або abs
float g = pow(max(x, 0.0), 2.2);
sin · cos · atan
одноаргументний atan(x) дає −π/2..π/2; для кута точки завжди двоаргументний
float a = atan(p.y, p.x);
min · max · abs · sign
каркас булевої логіки без if: max = АБО, min = І для SDF
float d = min(dCircle, dBox);
fwidth · dFdx · dFdy
похідна по екрану — ширина одного пікселя в одиницях поля. У WebGL1 треба ввімкнути розширення
#extension GL_OES_standard_derivatives : enable float w = fwidth(d); float m = smoothstep(w, -w, d);

SDF — знакові відстані

d < 0 всередині, d = 0 на межі, d > 0 зовні
SDF повертає відстань до фігури, а не «намальовано / не намальовано». Тому одна й та сама формула дає заливку, обведення, ореол і згладжений край — залежно від того, як з d зробити маску. Заливка: smoothstep(w,-w,d). Контур: abs(d)-th. Ореол: exp(-d*k).
circle(p, r)
найдешевша фігура; від неї починаються всі інші
float d = length(p) - r;
box(p, b)
коректний і зовні, і всередині — саме тому два доданки
vec2 q = abs(p) - b; float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
segment(p, a, b)
лінія зі скругленими кінцями; товщину задає віднімання радіуса
vec2 pa = p - a, ba = b - a; float h = clamp(dot(pa,ba)/dot(ba,ba), 0.0, 1.0); float d = length(pa - ba*h) - 0.02;
операції
об'єднання / перетин / віднімання — три рядки без жодного if
float uni = min(a, b); float ins = max(a, b); float sub = max(-a, b);
smin(a, b, k)
м'яке об'єднання: фігури зливаються краплею. k — радіус зливання
float smin(float a, float b, float k){ float h = clamp(0.5 + 0.5*(b-a)/k, 0.0, 1.0); return mix(b, a, h) - k*h*(1.0-h); }
малювання без драбинки
ширина краю в пікселях, а не в довільній константі — виглядає однаково на будь-якій щільності
float w = fwidth(d); float mask = smoothstep(w, -w, d); col = mix(col, shapeCol, mask);

Рандом, шум, fbm

детермінований «випадок» від координати
hash21(vec2) → float
класика на sin. Дешево, але результат залежить від точності GPU: на мобільних дає видимі смуги
float hash21(vec2 p){ return fract(sin(dot(p, vec2(12.9898,78.233))) * 43758.5453); }
hash без sin
стабільніший варіант — не залежить від реалізації sin у драйвері
float hash21(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
value noise 2D
рандом у вузлах сітки + згладжена інтерполяція. smoothstep-крива прибирає видиму решітку
float noise(vec2 p){ vec2 i = floor(p), f = fract(p); vec2 u = f*f*(3.0 - 2.0*f); return mix(mix(hash21(i), hash21(i+vec2(1,0)), u.x), mix(hash21(i+vec2(0,1)),hash21(i+vec2(1,1)), u.x), u.y); }
fbm(p, octaves)
сума октав: частота ×2, амплітуда ×0.5. Далі 5–6 октав різниці майже не видно, а ціна росте лінійно
float fbm(vec2 p){ float v = 0.0, a = 0.5; for(int i = 0; i < 5; i++){ v += a * noise(p); p *= 2.02; a *= 0.5; } return v; }
domain warping
подати шум сам у себе — з рівного туману виходять «димові» вихори. Найдешевший спосіб зробити картинку не схожою на шум
vec2 q = vec2(fbm(p), fbm(p + 5.2)); float v = fbm(p + 2.0*q + u_time*0.05);
цикли в ES 1.00
лічильник має бути порівняний з константою; i < octaves з uniform не скомпілюється. Обхід — константна межа + break за умовою
for(int i = 0; i < 8; i++){ if(float(i) >= u_oct) break; // ... }

Колір

де народжується різниця між «демка» і «картинка»
косинусна палітра · iq
чотири вектори замість градієнт-текстури. d зсуває фази каналів — звідси весь спектр
vec3 pal(float t, vec3 a, vec3 b, vec3 c, vec3 d){ return a + b*cos(6.28318*(c*t + d)); } // pal(t, vec3(.5), vec3(.5), vec3(1.), // vec3(0., .33, .67))
hsv2rgb
коли треба крутити саме відтінок, а не всі три канали
vec3 hsv2rgb(vec3 c){ vec3 p = abs(fract(c.xxx + vec3(0.,2./3.,1./3.))*6.0 - 3.0); return c.z * mix(vec3(1.0), clamp(p-1.0,0.0,1.0), c.y); }
гамма
лінійне світло в sRGB. Без цього все виглядає темнішим і брудним, особливо в градієнтах
col = pow(col, vec3(1.0/2.2));
тонмапінг
м'яко тягне яскраве до 1 замість жорсткого clamp — світіння перестає вигорати плямою
col = col / (1.0 + col);
вінєтка
кадрує погляд у центр; майже завжди покращує композицію задарма
col *= smoothstep(1.4, 0.3, length(p));
зерно / дизер
крихітний шум ламає банди в темних градієнтах — 8 біт на канал їх не витягує
col += (hash21(gl_FragCoord.xy) - 0.5) * 0.02;

Час і аудіо-реактивність

від статичної картинки до інструмента
безшовна петля
для GIF/WebM-експорту: усі функції часу мусять бути періодичні з тим самим T
float T = 8.0; float ph = TAU * mod(u_time, T) / T; float k = sin(ph); // період рівно T
float-дрейф
через годину iTime ≈ 3600, і mediump перестає розрізняти кадри — рух стає ривками
float t = mod(u_time, 600.0);
FFT зі слота · Shadertoy
аудіо приходить текстурою: рядок y≈0.25 — спектр, y≈0.75 — хвиля
float bass = texture(iChannel0, vec2(0.05, 0.25)).x; float wave = texture(iChannel0, vec2(uv.x, 0.75)).x;
згладжений удар
сирий FFT смикається покадрово. Пік — це різниця з підлогою, а не саме значення
float hit = smoothstep(0.35, 0.75, bass); float r = 0.3 + 0.12*hit;
параметр як uniform
будь-яка ручка з UI — це один uniform; шейдер не має знати, що її крутить палець, MIDI чи мік
uniform float u_warp; p += u_warp * 0.3 * vec2(sin(p.y*3.0+t), cos(p.x*3.0-t));
ease замість лінійності
smoothstep від часу читається як рух, лінійний ramp — як анімація в дебагері
float k = smoothstep(0.0, 1.0, fract(t*0.25));

Свій GLSL усередині hydra

setFunction — міст між шпаргалками
Кожна функція hydra — це шматок фрагментного шейдера. setFunction додає свій шматок під власним іменем, і далі він чейниться нарівно з osc чи kaleid. Усередині доступні: _st (координати), _c0 / _c1 (вхідні кольори), time, resolution. Тіло — це тіло функції: пишеться return vec4(...), оголосити поруч допоміжну функцію не вийде — інлайнити руками. hydra · custom glsl →
typeсигнатура по сутічейниться як
srcкоордината → колірджерело: myThing().out()
coordкоордината → координатагеометрія: osc().myWarp()
colorколір → колірколір: osc().myGrade()
combineдва кольори → колірblend: osc().myMix(noise())
combineCoordколір + координата → координатаmodulate: osc().myMod(noise())
src · клітинна сітка з рандомною яскравістю
setFunction({ name: 'cells', type: 'src', inputs: [{ type:'float', name:'n', default:8 }], glsl: ` vec2 g = _st * n; vec2 id = floor(g); float r = fract(sin(dot(id, vec2(12.9898,78.233)))*43758.5453); float k = 0.5 + 0.5*sin(time*2.0 + r*6.2831); return vec4(vec3(k*r), 1.0); ` }) cells(12).out()
coord · полярне викривлення (чейниться на будь-що)
setFunction({ name: 'polar', type: 'coord', inputs: [{ type:'float', name:'amt', default:1 }], glsl: ` vec2 p = _st - 0.5; float r = length(p); float a = atan(p.y, p.x) + r*amt*3.0; return vec2(cos(a), sin(a))*r + 0.5; ` }) osc(20, 0.05, 0.9).polar(1.5).out()

Готові патчі

формат Shadertoy — вставити й запустити
1 · плазма на косинусній палітрі
void mainImage(out vec4 O, in vec2 U){ vec2 p = (2.0*U - iResolution.xy)/iResolution.y; float t = iTime*0.3; float v = sin(p.x*3.0 + t) + sin(p.y*3.0 - t*1.3) + sin(length(p)*6.0 - t*2.0); vec3 col = 0.5 + 0.5*cos(6.28318*(vec3(0.0,0.33,0.67) + v*0.25)); col *= smoothstep(1.6, 0.2, length(p)); O = vec4(pow(col, vec3(1.0/2.2)), 1.0); }
2 · SDF-кільце з ореолом, пульс від баса (iChannel0 = аудіо)
void mainImage(out vec4 O, in vec2 U){ vec2 p = (2.0*U - iResolution.xy)/iResolution.y; float bass = texture(iChannel0, vec2(0.05, 0.25)).x; float hit = smoothstep(0.30, 0.75, bass); float d = abs(length(p) - (0.35 + 0.10*hit)) - 0.004; float w = fwidth(d); float core = smoothstep(w, -w, d); float glow = exp(-abs(d)*18.0) * (0.35 + 0.65*hit); vec3 col = vec3(0.05,0.02,0.08) + vec3(1.0,0.35,0.75)*glow + vec3(1.0)*core; O = vec4(pow(col/(1.0+col), vec3(1.0/2.2)), 1.0); }
3 · fbm + domain warping (дим)
float hash21(vec2 p){ p = fract(p*vec2(123.34,456.21)); p += dot(p, p+45.32); return fract(p.x*p.y); } float noise(vec2 p){ vec2 i = floor(p), f = fract(p); vec2 u = f*f*(3.0-2.0*f); return mix(mix(hash21(i), hash21(i+vec2(1,0)), u.x), mix(hash21(i+vec2(0,1)), hash21(i+vec2(1,1)), u.x), u.y); } float fbm(vec2 p){ float v=0.0, a=0.5; for(int i=0;i<5;i++){ v+=a*noise(p); p*=2.02; a*=0.5; } return v; } void mainImage(out vec4 O, in vec2 U){ vec2 p = (2.0*U - iResolution.xy)/iResolution.y * 2.0; float t = iTime*0.08; vec2 q = vec2(fbm(p + t), fbm(p + 5.2 - t)); float v = fbm(p + 2.4*q); vec3 col = mix(vec3(0.03,0.05,0.12), vec3(0.95,0.55,0.30), v*v); O = vec4(pow(col, vec3(1.0/2.2)), 1.0); }
4 · калейдоскоп по клітинках
void mainImage(out vec4 O, in vec2 U){ vec2 p = (2.0*U - iResolution.xy)/iResolution.y; float t = iTime*0.25; float n = 6.0; float a = mod(atan(p.y,p.x), 6.28318/n) - 3.14159/n; p = vec2(cos(a), sin(a)) * length(p); vec2 g = p*3.0 + t; vec2 id = floor(g); vec2 q = fract(g) - 0.5; float r = fract(sin(dot(id, vec2(12.9898,78.233)))*43758.5453); float d = length(q) - (0.12 + 0.22*r); float w = fwidth(d); float m = smoothstep(w, -w, d); vec3 col = mix(vec3(0.02,0.03,0.06), 0.5+0.5*cos(6.28318*(vec3(0.0,0.33,0.67)+r+t*0.2)), m); O = vec4(pow(col, vec3(1.0/2.2)), 1.0); }

Пастки

що ламається і чому
симптомпричинащо робити
чорний екран, помилок немадесь NaN: normalize(0), pow від'ємного, 0/0. NaN проходить множення і заражає весь піксельвивести проміжок: O = vec4(vec3(d), 1.0) і дивитись, де він зникає
коло — еліпсuv 0..1 без корекції аспекту(2.0*U − res.xy)/res.y
драбинка по краяхstep або порівняння замість згладженої маскиsmoothstep(w,−w,d) з w = fwidth(d)
смуги в темних градієнтах8 біт на канал + лінійний градієнтдодати зерно ±0.01 і гамму
шум інший на телефоніhash на sin + mediump: реалізація sin для великих аргументів різнитьсяhash без sin, або highp
тремтіння через кілька хвилинчас росте, mediump втрачає розрізнення сусідніх кадрівmod(u_time, 600.0)
21 fps без видимої причиниfbm у 8+ октав, length у циклі, гілки if, які на GPU виконуються обидвіменше октав, dot(p,p) замість length, гілки → mix/step
працює локально, не в Shadertoy (або навпаки)texture2D проти texture, gl_FragColor проти outвизначитись із версією; ES 3.00 = #version 300 es першим рядком
дірка в фігурі на стикуmax(-a, b) переплутано порядком аргументіввіднімається ЗАВЖДИ той, що зі знаком мінус

Помилки компілятора

переклад із драйверської на людську
повідомленнящо насправді
no matching overloaded function foundтипи аргументів не ті: mix(vec3, float, float) або 1 замість 1.0
cannot convert from 'int' to 'float'та сама зникла крапка
No precision specified for (float)у фрагментному шейдері ES 1.00 нема precision mediump float;
'assign' : l-value requiredзапис у свізл із повтором (col.rr = ...) або у varying/uniform
Loop index cannot be compared with non-constantES 1.00: межа циклу мусить бути константою
'texture' : no matching overloaded functionES 1.00 очікує texture2D
#version directive must occur before anything elseперед #version 300 es є коментар, пробіл або BOM
undeclared identifier: 'fwidth'WebGL1 без #extension GL_OES_standard_derivatives : enable
'return' : function return is not matching typeу hydra setFunction тип coord повертає vec2, решта — vec4

Що брати під задачу

таблиця рішень
задачаінструментчому
швидко накидати ідею, поділитись лінкомShadertoyнуль налаштування, готові uniform, аудіо і відео в слотах
візуал у джемі під музикуHydraланцюг замість шейдера; свій GLSL — точково через setFunction
візуал з телефона, руками, з експортомtouchvizтач, гіроскоп, мік; віддає готовий .glsl
шейдер у власному застосункусирий WebGL2свої uniform, свої проходи, нічия чужа обгортка
треба фігури, а не текстуриSDFодна формула дає заливку, контур і ореол
треба органіка, а не геометріяfbm + domain warpдві октави шуму дають те, чого не дасть жоден набір примітивів
треба зациклений кліпперіод T у всіх функціях часуінакше шов буде видно на кожному повторі