Av Petter Amundsen
Hittil i år har det vært snø en gang, hvertfall der eg bur. Det var ogso berre såvidt innom før det blei til slaps, og so var det vekke igjen. Men eg har ogso vært innom thebookofshaders.com og det er eit emne eg syntes er veldig interessant. Derfor har jeg tenkt å kombinere disse to tankene og lage vår egen snø!
Ein shader er ein liten programbit som avgjer fargen på kvar piksel i eit bilete eller ein animasjon. Shaderen kan endre bildet ved å endre fargar og mønster.
Slik kan ein lage effektar som glød, bølgjer, forvrenging eller mjukare overgangar.
Det som er spesielt med shadere er at den samme koden blir kjørt for kvar piksel i bildet. Det er ingen minne frå frame til frame, og inputen til kvar piksel er derfor det samme, uansett kva ein vil teikne i den pikselen.
Eg skal ikkje gå sjukt inn i detaljane her, for eg kan ikkje so utruleg mykje sjølv, so berre slapp av.
I vårt tilfelle skal me berre bruke det til å lage snø. Me byrjer simpelt og lager eit enkelt snøfnugg.
For å gjere det skal me lage ein sirkel i midten av bildet. Det gjer me enkelt ved at kvar piksel rekner ut kor langt unna sentrum det er, og dersom det er nære nok, set me fargen til kvit eller noko lignande.
uniform vec2 u_resolution;
void main() {
// gl_FragColor.xy er posisjonen til pikselen me skal teikna i bildet.
// F.eks (150, 75). Vi vil ha dette tallet mellom 0 og 1, så vi
// deler posisjonen på oppløsningen
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
// Så rekner me ut distansen til sentrum av bildet: (0.5, 0.5)
float dist = distance(uv, vec2(0.5, 0.5));
// Sett isSnow til 1 dersom distancen er mindre enn 0.05
float isSnow = smoothstep(0.05, 0.045, dist);
// Sett fargen til svart så vi ser snøen
vec4 color = vec4(0., 0., 0., 1.);
// Pluss på isSnow, som nå er 1. dersom det skal være snø og 0 om ikke.
// Multipliser med 0.3 for å få litt grå farge
gl_FragColor = vec4(color + isSnow * .3);
}
I shadere bruker man ofte step funksjonen for å bestemme om noko skal være av eller på. Det fungerer ofte som ein if setning, men hvor hver piksel som kalkuleres bruker akkurat like mange instrukser, kontra en vanlig if hvor det blir flere dersom man går inn i if blokken.
step() funksjonen tar 2 argumenter, det første er terskelen, mens det andre er verdien man sjekker mot terskelen. Verdier som er under terskelen returnerer 0, mens verdier som er like eller over returnerer 1.
Dette kan vi for eksempel bruke til å lage en sirkel, ved å sjekke om distansen til sentrum er mindre enn størrelsen vi vil ha på sirkelen.
Her har vi derimot brukt smoothstep(), som er veldig lik. Den tar i mot eit ekstra argument og interpolerer mellom 0 og 1 mens verdien er mellom disse to argumentene. Dette bruker me til å lage ein sirkel som har ein mjukare kant, enn dersom ein hadde brukt step funksjonen.
Me håpte på litt meir snø enn berre eit snøfnugg, så no skal me lage fleire. Dette gjer me rett og slett ved dele opp bildet vårt i fleire mindre, heilt like deler.
Først spesifiserer me kor mange snøfnugg me vil ha. So ganger me koordinatene med dette, og då kan me få nye koordinater for kvar litle bilde/celle me har laga ved å ta ut desimalene i koordinatene med fract() funksjonen.
So bruker me desse koordinatene i stedet for de originale koordinatene.
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
// Ny kode
float num_flakes = 100.0;
float cr = sqrt(num_flakes);
vec2 grid = uv * vec2(cr, cr);
vec2 frac = fract(grid);
// Samme kode, med litt modifisert snøfnugg størrelse
float flake_size = 0.15;
float dist = distance(frac, vec2(0.5, 0.5));
float isSnow = smoothstep(flake_size + 0.05, flake_size - 0.05, dist);
vec4 color = vec4(0., 0., 0., 1.);
gl_FragColor = vec4(color + isSnow * .3);
}
Snø, eller sirkler, som berre heng i lufta er ikkje særleg interessant, so no skal me få det til å falle. Dette gjer me ved å modifisere posisjonen me er på no, altså uv, med tiden. Tiden har vi tilgjengelig via ein uniform kalt u_time. Ein uniform er ein input til shaderen. Men, siden alle piksler må kjøyre same kode, er den heilt lik for alle piksler.
Me legg til tid + nokon ekstra konstanter som me definerer som representerer styrken og retningen til vinden. Enkelt og greit.
#define WIND_DIR vec2(.0, .5)
#define WIND_STRENGTH .25
uniform float u_time;
uniform vec2 u_resolution;
void main() {
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
// Legg til tid og vind her
uv += u_time * WIND_DIR * WIND_STRENGTH;
float cr = sqrt(num_flakes);
vec2 grid = uv * vec2(cr, cr);
vec2 frac = fract(grid);
float dist = distance(frac, vec2(0.5, 0.5));
float isSnow = smoothstep(flake_size + 0.05, flake_size - 0.05, dist);
vec4 color = vec4(0., 0., 0., 1.);
gl_FragColor = vec4(color + isSnow * .3);
}
Det er fortsatt noko trist snø. Det er berre eit lag og den faller heilt jevnt. Først skal me lage litt fleire lag.
Me flytter koden for å lage snøfnugg ut i ein funksjon og so lar me input til funksjonen definere størrelsen på snøfnuggene, samt vindstyrken.
So kaller me denne funksjonen 3 ganger for å få litt dybde.
#define WIND_DIR vec2(.0, .5)
#define WIND_STRENGTH .25
uniform float u_time;
uniform vec2 u_resolution;
float snow_layer(vec2 uv, float flake_size, float num_flakes, float wind_mod) {
uv += u_time * WIND_DIR * WIND_STRENGTH * wind_mod;
float cr = sqrt(num_flakes);
vec2 grid = uv * vec2(cr, cr);
vec2 frac = fract(grid);
float dist = distance(frac, vec2(0.5, 0.5));
float isSnow = smoothstep(flake_size + 0.05, flake_size - 0.05, dist);
return isSnow;
}
void main() {
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
float isSnow = snow_layer(uv, 0.08, 150., 1.);
isSnow += snow_layer(uv, 0.06, 225., .5);
isSnow += snow_layer(uv, 0.05, 300., .25);
vec4 color = vec4(0., 0., 0., 1.);
gl_FragColor = vec4(color + isSnow * .3);
}
I starten lagde me fleire snøflak ved å dele bildet inn i fleire små deler, og det blir dessverre litt for simpelt. Me har ikkje nok spelerom til at fordelingen av snø er truverdig. So no skal me gå vekk frå den metoden og over til å bruke Voronoi algoritmen.
Kort fortalt vil det sei at me skal lage den samme griden me gjorde tidlegare, og so "markere" eit punkt i kvar. Kvar piksel skal no sjekke kva punkt i dei 9 næraste cellene som er det næraste punktet. So bruker me distansen til dette punktet i stedet for sentrum av cellen. Dette fikser eit problem me ville hatt med forrige variant der snøfnugg som vart plassert for langt mot kanten av cellen ville blitt kuttet.
#define WIND_DIR vec2(.0, .5)
#define WIND_STRENGTH .25
uniform float u_time;
uniform vec2 u_resolution;
vec2 random2( vec2 p )
{
return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}
float voronoi(in vec2 x)
{
vec2 cellId = floor(x);
vec2 f = fract(x);
vec2 mg, mr;
float minDist = 8.0;
for( int j=-1; j<=1; j++ )
for( int i=-1; i<=1; i++ )
{
vec2 nabo = vec2(i, j);
vec2 punkt = random2( cellId + nabo );
vec2 r = nabo + punkt - f;
float dist = dot(r,r);
if( dist<minDist )
{
minDist = dist;
mr = r;
mg = nabo;
}
}
return minDist;
}
float snow_layer(vec2 uv, float num_flakes) {
float wind_mod = 70. / num_flakes;
uv += u_time * WIND_DIR * WIND_STRENGTH * wind_mod;
float cr = sqrt(num_flakes);
vec2 grid = uv * vec2(cr, cr);
float d = voronoi(cr * uv);
float flake_size = 0.25 / num_flakes;
float smooth = 1.25 * flake_size;
float dist = length(d);
float isSnow = smoothstep(flake_size + smooth, flake_size - smooth, dist);
return isSnow;
}
void main() {
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
float isSnow = snow_layer(uv, 70.);
isSnow = max(snow_layer(uv, 85.), isSnow);
isSnow = max(snow_layer(uv, 100.), isSnow);
isSnow = max(snow_layer(uv, 130.), isSnow);
isSnow = max(snow_layer(uv, 150.), isSnow);
isSnow = max(snow_layer(uv, 140.), isSnow);
isSnow = max(snow_layer(uv, 160.), isSnow);
isSnow = max(snow_layer(uv, 200.), isSnow);
isSnow = max(snow_layer(uv, 220.), isSnow);
vec4 color = vec4(0., 0., 0., 1.);
gl_FragColor = vec4(color + isSnow * .3);
}
Eg endra ogso litt på resten av logikken her, og gjorde størrelsen på snøfnugga, samt vindstyrken basert på antall snøfnugg i det laget.
Til sist skal me berre pynta litt på ting. Me skal legge til horisontal bevegelse og variere størrelsen til snøfnugga innad i laga med snø.
#define WIND_DIR vec2(.0, .5)
#define WIND_STRENGTH .25
uniform vec2 u_resolution;
uniform float u_time;
vec2 random2( vec2 p )
{
return fract(sin(vec2(dot(p,vec2(127.1,311.7)),dot(p,vec2(269.5,183.3))))*43758.5453);
}
float voronoi(in vec2 x)
{
vec2 cellId = floor(x);
vec2 f = fract(x);
vec2 mg, mr;
float minDist = 8.0;
for( int j=-1; j<=1; j++ )
for( int i=-1; i<=1; i++ )
{
vec2 nabo = vec2(i, j);
vec2 punkt = random2( cellId + nabo );
vec2 r = nabo + punkt - f;
float dist = dot(r,r);
if( dist<minDist )
{
minDist = dist;
mr = r;
mg = nabo;
}
}
return minDist;
}
float snow_layer(vec2 uv, float num_flakes) {
float layer = 70. / num_flakes;
float wind_mod = 70. / num_flakes;
uv += u_time * WIND_DIR * WIND_STRENGTH * wind_mod;
float dir_var = 1. + sin(uv.x/100.+u_time)/4.;
// Legg til litt horisontal bevegelse også
uv += vec2(dir_var/5., dir_var / 100.);
float cr = sqrt(num_flakes);
vec2 grid = uv * cr;
float d = voronoi(cr * uv);
// Varier størrelsen med 25%
vec2 size_var = random2(floor(grid)) * .25;
float flake_size = (0.25 + size_var.x) / num_flakes;
float smooth = .75 * flake_size;
float dist = length(d);
float isSnow = smoothstep(flake_size + smooth, flake_size - smooth, dist);
return isSnow;
}
void main() {
vec2 uv = gl_FragCoord.xy/u_resolution.xy;
float isSnow = snow_layer(uv, 70.);
isSnow = max(snow_layer(uv, 85.), isSnow);
isSnow = max(snow_layer(uv, 100.), isSnow);
isSnow = max(snow_layer(uv, 130.), isSnow);
isSnow = max(snow_layer(uv, 150.), isSnow);
isSnow = max(snow_layer(uv, 140.), isSnow);
isSnow = max(snow_layer(uv, 160.), isSnow);
isSnow = max(snow_layer(uv, 200.), isSnow);
isSnow = max(snow_layer(uv, 220.), isSnow);
vec4 color = vec4(0., 0., 0., 1.);
gl_FragColor = vec4(color + isSnow * .3);
}
Det var alt eg hadde planlagt i år! Håpar maskina di klarte seg greit gjennom denne sida.
Syntes du dette var interessant anbefaler eg å sjekke ut https://thebookofshaders.com som forklarer det på ein måte eg forstod nok av til å lage denne luken. Der finner du også interaktive forklaringer som lar deg fikle litt med tall med live oppdatering.
Vil du fikle med talla i desse kodesnuttane også så kan du det på http://editor.thebookofshaders.com/ . Berre kopier kodesnutten inn, og legg dette i toppen:
#ifdef GL_ES
precision mediump float;
#endif
// Resten av shader koden