Programowanie Gier -> Wykład: Shadery GLSL (OpenGL Shading Language)

Kawałki poniższego streszczenia skopiowałem z własnego dawnego opowiadania o shaderach na seminarium z grafiki. Tam też znajdziecie nieco linków i uwag o starych shaderach asemblerowych (ARB), które są już generalnie historią i na które nie było czasu w trakcie wykładu.

1. What are shading languages, in short

OpenGL program is basically (note: forget for a sec that in OpenGL 3.x glBegin/End is no more)

  glLoadMatrix, glMultMatrix (and shortcuts like glLoadIdentity, glTranslate, glScale...)

  glBegin(...)

    gl(... some vertex attrib, like glTexCoord, glMultiTexCoord, glMaterial...)
    glVertex(...);

    gl(... some vertex attrib, like glTexCoord, glMultiTexCoord, glMaterial...)
    glVertex(...);

    gl(... some vertex attrib, like glTexCoord, glMultiTexCoord, glMaterial...)
    glVertex(...);

    ....

  glEnd();

For each glVertex, some work is done.

  1. transform it by projection * modelview matrices,
  2. calculate per-vertex values like color from lighting
      color =
        material emission +
        scene ambient * mat ambient +
        for each light: (
          light ambient * mat ambient +
          light diffuse * mat diffuse * diffuse factor +
          light specular .... etc.).
    

This work may be replaced by vertex shader, to transform in any way we like, calculate color by any equation we like (using OpenGL lights parameters or not) and some other.

When vertexes make primitives, each primitive is checked (for backface culling) and is rasterized. Note that some vertexes are shared by more than one primitive, e.g. triangle strips, fans etc. The idea of these primitives is that the per-vertex work (whether fixed-function or vertex shader) is done only once, and results are reused.

Fragment = same thing as pixel by default, although some operations like glPixelZoom change this correspondence. Rasterization calculates coordinates of fragments, each fragment gets as input some interpolated values (for example, color results of lighting calculation from paragraph above are interpolated for fragment).

Then for each fragment some work is done. In fixed-function pipeline, this is mostly to mix color with textures (all the multitexture operations set up by glTexEnv are done here) and apply fog parameter. This work may be replaced by fragment shader.

There are also geometry shader, for later...

Notes:

  • Both shaders replace fixed-function work. There's generally no way to say "do exactly the same work as when fixed-function was used, and then pipe the results through this shader...". (there are special commands/variables to do inside shader some part of this, like ftransform and lightProducts, but that's about it).

    This is by design, the idea is that using shaders may be actually faster than letting fixed-function pipeline do it's job. That's because fixed-function pipeline does quite a lot of complicated work, and in most cases we don't need it. IOW, shaders are usually written with very specific environment in mind (e.g. "I will use only one light source, and without attenuation").

  • In GLSL < 1.40:

    There's no requirement to replace both vertex and fragment shaders. It's Ok to replace only one of them, and let the other do it's fixed-function job.

    ... but it's often useful to replace both. When we write both vertex and fragment shaders, we're able to pass from vertex shader to fragment shader any value to interpolate. This is called "varying" variable.

    For example, Phong shading: trivial vertex program to just pass normal for interpolation, and fragment program to calculate lighting per-pixel.

  • In GLSL 1.40:

    You can't mix fixed-function stages with shaders. E.g. if you provide fragment shader, you should also provide vertex shader. Mixing is deprecated in GLSL 1.30, disallowed in GLSL 1.40.

2. Język GLSL

  • Krótkie demo niektórych trywialnych shaderów z kambi_vrml_test_suite, demo trywialnych modyfikacji.

  • GLSL specs are here.

  • And here.

    Note that spec referenced there, and related "quick ref guide" are for GLSL 1.20, not newest 1.40. Actually, it's not *that* terrible, because a lot of existing hardware is still on GLSL 1.20. So you may be Ok with targetting 1.20 anyway (as long as you know what changes in 1.30 and 1.40). W czasie wykładu postaram się wspominać o rzeczach specyficznych dla wersji GLSL, i postaram się opowiadać tak żebyście wiedzieli jak pisać i dla GLSL 1.20 i dla nowszych.

    Krótkie notki o nowych wersjach GL 3.x/GLSL 1.30/1.40 na NVidie. W skrócie, GeForce 8000 >= ma GL 3.0 z GLSL 1.30, GL 3.1 z GLSL 1.40. Na starszych kartach należy oczekiwać tylko GL 2.x i GLSL 1.20. Pozostawiam Wam decyzję czy obsługiwać stare GPU czy nie.

Język C-podobny. Poniżej garść faktów i specyfiki GLSL, zakładam że elementarny C znamy *wszyscy* :)

Mamy "void main(void)" jaką główne funkcje dla shadera (vertex, albo fragment).

Vertex shader przede wszystkim musi ustawić gl_Position.

Fragment shader przede wszystkim musi ustawić gl_FragColor albo gl_FragData (dla renderowania do wielu buforów). Ale tylko jedno, i dokładnie jedno z nich... W GLSL > 1.20 są deprecated, najlepiej je zadeklarować samemu.

Basic demos w trakcie opowiadania:

Typy:

  • Nie ma stringów, bo przecież nie ma I/O.

  • Są prawdzwie boole, unlike C.

  • Inty są zawsze 32-bitowe. int (signed), uint, chociaż to zazwyczaj bez znaczenia: over/underflow robią wrap bez żadnych błędów, tak jak w C (i unlike wiele innych języków).

  • Wewnętrzne floaty nie mają zdefiniowanej precyzji. NVidia pozwala używać half floatów explicite (typy half3, etc.) Patrz nvidia glsl release notes.

    GLSL 1.40 ma precision qualifiers (lowp, mediump, highp) i ustalacze default precision ("precision lowp float;") ale (chyba) nigdzie nie jest gwarantowane czy/jak te prec qualifiers będą honorowane. Zapewne dla nvidii "lowp float" będzie tym samym co "half".

  • Mamy struktury, arrays 1-wym (chociaż macierze są 2-wym), arrays mają metodę length.

  • Są wbudowane typy wektorowe i macierzowe, których naturalnie należy używać. Większość praktycznych shaderów to operacje na vec3, vec4.

    Poza [0..3], do wektorów można się też dobierać przez xyzw, strq, rgba (w zależności od tego czy traktujemy je jak pozycje/kierunki, współrzędne tekstury, kolory. Dla shadera to kompletnie bez znaczenia, v.x to to samo co v.r). "Swizzling" komponentów jest praktycznie za darmo, wbudowany w hardware przepisujący, więc używać bez zachamowań.

    Mamy macierze (2-4)x(2-4). Column-major, jak wszędzie w OpenGL, więc pierwszy indeks to kolumna.

    Mamy konstruktory i konwertery, więc możemy zainicjować wektor, stworzyć nowy, np. vec3 yellow = vec3(1.0, 1.0, 0.0);, rzutować: vec4 yellow_opaque = vec4(yellow, 1.0);

  • Specjalne typy dla tekstur jak sampler2D, samplerCube (cube mapy), sampler2DDepth (dla ARB_depth_texture znane z poprz. wykładu o shadow maps), sampler2DRect (dla ARB_texture_rectangle), sampler3D, etc.

    Wbudowane funkcje do próbkowania ich jak texture2D(tex, sample), jeszcze wspomnimy o ich szczególnych odmianach później. Jest zrozumiałe że nie możemy się "dobrać" do wewnętrznej zawartości samplera, możemy tylko go próbkować na różne sposoby przez odpowiednie funkcje GLSL.

    Honorują ustawienia filtrowania, mipmapy etc. kiedy używane we fragment shader. Tekstury depth honorują ustawienia GL_TEXTURE_COMPARE_MODE_ARB, mają specjalne funkcje shadowXxx do próbkowania.

    Generalnie, wszystko co ustawiamy przez glTexParameter jest honorowane.

    Z kolei np. rzeczy ustawiane przez glTexEnv są ignorowane --- to implementujemy sami w shaderach.

    Samplery z punktu widzenia API OpenGLa ustawiamy jak inty, gdzie numer n oznacza że przekazujemy teksturę zbindowaną do glActiveTexture(GL_TEXTUREn_ARB); Samplery muszą być uniform (co to znaczy, i jak dokładnie przekazywać dane do shaderów --- za sekundę).

Rodzaje zmiennych, przekazywanie do/z shadera:

  • oczywiście są lokalne i globalne. Generalnie, state przekazywany pomiędzy CPU a GPU i vertex a fragment jest przez globalne zmienne "in", "out".

  • Mamy też trochę wbudowanych zmiennych opisujących stan OpenGL (chociaż GLSL 1.40 sporą część z nich zrobił deprecated i tylko pod ARB_compatibility).

  • const, in (z ew. "centroid"), out (z ew. "centroid")

  • uniform: przekazywanie wartości które są stałe w trakcie wywołania draw (pomiędzy glBegin/End w GL < 3.1, albo po prostu nie mogą się zmienić w glDrawElements etc.)

    (Uniform blocks nowe w GLSL 1.40, chyba nie omówimy teraz.)

  • przekazywanie wartości per-vertex do vertex shadera: "attribute" w GLSL 1.20, w nowszych po prostu "in".

  • przekazywanie wartości per-fragment z vertex do fragment shadera (z interpolacją): "varying" w GLSL 1.20, w nowszych "out" w vs, "in" w fs.

    Mogą mieć dodatkowo "smooth" (persp interp), "noperspective" (linear interp), "flat" (zgodnie z zasadami glShadeModel(GL_FLAT), czyli wartości na niektórych vertexach będą ignorowane; generalnie ostatni vertex determinuje wartość dla prymitywu).

    Dla "smooth" i "noperspective", istotny może być też "centroid" kiedy używamy multisampling. Generalnie, "centroid" zapewnia że interpolowana wartość jest rzeczywiście ze środka renderowanego prymitywu (przy multi-sampling, to może nie być spełnione bez centroid). Używać tylko kiedy rzeczywiście potrzebne.

  • parametry funkcji mogą być const, in, out, inout (zauważyliście chyba że nie ma pointers?). "in" jest default.

  • Invariance. Kiedy uruchamiamy dwa razy ten sam kod wkompilowany w różne programy, nawet z dokładnie takimi samymi wartościami na wejściu, takimi samymi ustawieniami tekstur etc., nie ma gwarancji że wyniki są 100% takie same. "invariant" pozwala to wymusić (za cenę straty pewnych optymalizacji). Typowe zastosowanie: multi-pass kiedy musimy pokryć te same pixele, stąd najczęściej gl_Position może wymagać invariant.

    Było też ftransform() które gwarantowało invariance, także z tym co robi fixed-function pipeline, w nowszych jest tylko pod ARB_compatibility.

    Jest "#pragma STDGL invariant(all)", dobre żeby szybko zobaczyć dla debug czy nasze problemy wynikają z invariance.

Operatory:

  • Wszystko jak w C. Assignment =, porównanie ==. Są bit-wise shifts/logical, są +=, *= etc.

  • Generalnie, działają component-wise na wektorach/macierzach, poza naturalnymi wyjątkami (mnożenie macierz * macierz).

  • pamiętajcie o funkcyjnym "if": ?: Na shaderach, to może być duża pomoc, bo unika branching (jeżeli oba argumenty są gotowe). Branching może nas boleć na shaderach, więcej o tym za chwilę. Chociaż optymalizatory generalnie umieją zamienić normalnego ifa na ?: wewnętrznie.

Control flow:

  • Jak w C: if-else, switch-case-default (od GLSL 1.30), for, while, do-while, return, break, continue.

  • Uwaga: starsze shadery robią branche w straszny sposób, a pętle w jeszcze gorszy. Branche są robione oba (i wyniki jednego są, intuicyjnie, mnożone przez 0), pętle są rozwijane do "wystarczających" (zazwyczaj, a czasami nie) wielkości. Patrz shading_langs/README po moje zabawy demonstrujące to.

    Nowsze GPU mogą robić to prawdziwie, ale ciągle może to zabić szybkość: shadery na GPU są super bo działają równolegle. Kiedy shadery nie są wykonywane razem (np. fragment shadery pracujące nad tym samym prymitywem w tym samym fragmencie okna) szybkość może spaść straszliwie.

  • "discard" we fragment shader, pozwalające robić coś jak alpha_test, ale naturalnie używając dowolnego warunku. (TODO: hm, a czy shadery honorują OpenGLowe alpha test?) Naturalnie, patrz wyżej (mogą zabić równoległość, albo sprawić że shader będzie "kręcił się nie robiąc nic"), ale mogą też być użyteczne.

  • Można oczywiście deklarować własne funkcje. Tu chyba wszystko jak w C, parametry funkcji in, out, inout, const jak wspominałem. Functions overloading jest wbudowane.

Built-in functions: browse.

Mamy preprocesor.

  • shader powinien deklarować wersję przez #version, chyba że jest na 1.10
  • można wymagać #extension języka shaderów
  • można sprawdzać wersję języka przez #if i __VERSION__, więć można pisać shadery pod kilka wersji

Podsumowanie ważnych IMO rzeczy deprecated w 1.30, removed w 1.40:

  • Co to jest invariance: ftransform w GLSL 1.20 -> invariant on vertex output
  • attribute/varying keywords -> in/out
  • wiele wbudowanych atrybutów/uniforms znika -> używaj własnych uniform/attribute

3. API OpenGLa do shaderów

(Krótko i skrótowo, bo to wszystko naturalne):

  - OpenGL API for shaders (with OpenGL 2.0 standard functions,
    there are mostly equivalent functions for ARB extension):

    /* Preparation: */

    prog := glCreateProgram();

    shader := glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(shader, ... pass strings containing shader source ...);
    glCompileShader(shader);
    glAttachShader(program, shader);

    (same for GL_FRAGMENT_SHADER)

    glLinkProgram(program);


    /* Usage: */

    glUseProgram(program);
    ....
    glUseProgram(0); // back to fixed-function pipeline

    /* End: */

    /* glDetachShader, glDeleteShader(...) if you like to be clean */
    glDeleteProgram(prog);

    More details:
    - after compilation, you should check
      glGetShaderiv(shader, GL_COMPILE_STATUS, ...)
      eventual error message is in glGetShaderInfoLog

    - after linking, you should check
      glGetProgramiv(program, GL_LINK_STATUS, ...)
      eventual error message is in glGetProgramInfoLog

- uniform variables (glGetUniformLocation, glUniform)
  (settable per-object, that is only outside glBegin / glEnd)

- attribute variables (glGetAttribLocation, glVertexAttrib)
  (settable per-vertex, that is possibly within glBegin / glEnd)

Patrz glshaders.pas który opakowuje to w elegancką klasę TGLSLProgram (pokazuje jak używać i GL core 2.0 i rozszerzeń ARB do GLSL).

4. Demos

Trochę moich prostych ale już bardziej użytecznych:

Trochę moich (nieznacznie) mniej prostych:

  • Shader do environment mapping, reflection, wody, z refraction.

    Wytłumaczyć textureXxx wersje z Bias, Lod. Pokazac jak biasem można zrobic odbicie wody bardziej / mniej ostre. Widać też np. że interpolacja pomiędzy różnymi ścianami cube mapy nie działa.

    Play with scaling texture coords, makes easily different waves types.

    GLSL 1.40 daje też wersje offset (przydatne do typowych sytuacji gdy próbujemy w okolicach jednego texela, potencjalnie optymalizowalne lepiej) i grad.

    water_reflections/.

  • Rzut oka na shader do bump mapping z parallax.

  • Rzut oka na dynamic amb occlusion? Technika sama w sobie zasługuje na 2 godziny tłumaczenia (i takie jej kiedyś zrobiłem :), zresztą podobnie jak bump mapping z parallax), więc naprawdę rzucimy okiem bardzo szybko.

Dość moich, teraz coś z http://www.humus.name/:

  • Volume LightMapping

    (idea bardzo prosta: użyj przygotowanej tekstury 3D aby znać cienie na scenie. Potrzebujesz gotowej tex 3d (pokaż DDS) (jak go zrobić, każdy może sobie dopowiedzieć: prosty ray-tracerek rzucający promienie do świateł naokoło (próbkujący je jeśli są area).) Na vertex shader oblicz lightCoord, czyli współrzędna dla tex3d (ona jest w world space, tak jak tex3d, stąd np.

      pos = modelMat * pos;
      ...
      lightCoord = pos.xyz * scale + 0.5;
      

    dla ufo które się rusza). Na fragment shader weź zinterpolowaną pozycję lightCoord i

      float shadow = texture3D(LightVolume, lightCoord).x;
      gl_FragColor = shadow * ...;
      

    Filtrowanie tex3d przez GL zapewni gładkość cieni.

  • Volumetric Fogging 2

    Znowu pomysł z tex3d. Mamy light map w 3d jak poprzednio, mamy też fog intensity w 3d.

      // Volumetric fog computation
      for (int i = 0; i < COUNT; i++){
              fogCoord += fogScaleBias.w * dir;
              shadowCoord += shadowScale * dir;
    
              float fog = texture3D(Fog, fogCoord).x;
              float shadow = texture3D(LightMap, shadowCoord).x;
    
              // Compute weighting factors. This implements the commented out lerp more efficiently.
              float x = 1.0 - fog * n;
              a *= x;
              b = lerp(shadow, b, x);
      }
      
      a ~= ile widać zza mgły
      b ~= ile mam koloru mgły
    
      x ~= ile nie mam koloru mgły... (bliżej 0 -> więcej mgły)
      b = lerp(shadow, b, x); ~=
        gdy nie mam koloru mgły (x~=1), b=b czyli nic się nie zmienia
        gdy mam mgłę (x~=0), to widzę oświetlony (shadow proporcjonalny do jasności, wbrew nazwie) kolor mgły.
      

    Uwaga: "lerp" to Cg-ism. W GLSL powinniśmy używać "mix". lerp(x, y, alpha) = mix(x, y, alpha);

    Disclaimer: piszę tą interpretację po szybkim rzucie oka na kod. Mogę się mylić...

    Trochę uwag omawiających tą implementację jest też na http://www.evl.uic.edu/sjames/cs525/shader.html


MichalisWiki: II/ProgramowanieGier/WykladGLSL (last edited 2009-12-17 21:40:00 by MichalisKambi)