Tworzenie shaderów za pomocą Babylon.js

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ Shadery to kluczowa koncepcja, jeśli chcesz uwolnić surową moc swojego GPU. Pomogę Ci zrozumieć, jak działają, a nawet poeksperymentować z ich wewnętrzną mocą w łatwy sposób, dzięki Babylon.js . Zanim zaczniemy eksperymentować, musimy zobaczyć, jak rzeczy działają wewnętrznie. Kiedy masz do czynienia z akceleracją sprzętową 3D, będziesz miał do czynienia z dwoma procesorami: głównym procesorem i GPU. GPU to rodzaj niezwykle wyspecjalizowanego procesora.

Shadery to kluczowa koncepcja, jeśli chcesz uwolnić surową moc swojego GPU. Pomogę Ci zrozumieć, jak działają, a nawet poeksperymentować z ich wewnętrzną mocą w łatwy sposób, dzięki Babylon.js.

Jak to działa?

Zanim zaczniemy eksperymentować, musimy zobaczyć, jak rzeczy działają wewnętrznie.

Kiedy masz do czynienia z akceleracją sprzętową 3D, będziesz miał do czynienia z dwoma procesorami: głównym procesorem i GPU. GPU to rodzaj niezwykle wyspecjalizowanego procesora.

Dalsze czytanie na SmashingMag:

  • Tworzenie wieloplatformowej gry WebGL za pomocą Babylon.js
  • Korzystanie z interfejsu API gamepada w grach internetowych
  • Wprowadzenie do modelowania wielokątnego i Three.js
  • Jak stworzyć responsywną 8-bitową maszynę perkusyjną?
Więcej po skoku! Kontynuuj czytanie poniżej ↓

GPU to maszyna stanu, którą konfigurujesz za pomocą procesora. Na przykład procesor skonfiguruje GPU do renderowania linii zamiast trójkątów; określi, czy przejrzystość jest włączona; i tak dalej.

Gdy wszystkie stany są ustawione, procesor może zdefiniować, co ma być renderowane: geometrię.

Geometria składa się z:

  • lista punktów zwanych wierzchołkami i przechowywanych w tablicy zwanej buforem wierzchołków,
  • lista indeksów definiujących twarze (lub trójkąty) przechowywane w tablicy o nazwie index buffer.

Ostatnim krokiem dla CPU jest zdefiniowanie sposobu renderowania geometrii; do tego zadania procesor zdefiniuje shadery w GPU. Shadery to fragmenty kodu, które GPU wykona dla każdego z wierzchołków i pikseli, które ma renderować. (Wierzchołek — lub wierzchołki, gdy jest ich kilka — jest „punktem” w 3D).

Istnieją dwa rodzaje shaderów: vertex shadery i pixel (lub fragment) shader.

Potok graficzny

Zanim przejdziemy do shaderów, cofnijmy się. Aby renderować piksele, GPU przyjmie geometrię zdefiniowaną przez procesor i wykona następujące czynności:

  • Używając bufora indeksu, zbierane są trzy wierzchołki, aby zdefiniować trójkąt.
  • Bufor indeksów zawiera listę indeksów wierzchołków. Oznacza to, że każdy wpis w buforze indeksu jest numerem wierzchołka w buforze wierzchołków.
  • Jest to naprawdę przydatne, aby uniknąć duplikowania wierzchołków.

Na przykład następujący bufor indeksu jest listą dwóch twarzy: [1 2 3 1 3 4]. Pierwsza ściana zawiera wierzchołek 1, wierzchołek 2 i wierzchołek 3. Druga ściana zawiera wierzchołek 1, wierzchołek 3 i wierzchołek 4. Tak więc w tej geometrii są cztery wierzchołki:

(Wyświetl dużą wersję)

Vertex Shader jest stosowany do każdego wierzchołka trójkąta. Podstawowym celem Vertex Shader jest wytworzenie piksela dla każdego wierzchołka (rzutu wierzchołka 3D na ekranie 2D):

(Wyświetl dużą wersję)

Używając tych trzech pikseli (które definiują trójkąt 2D na ekranie), GPU będzie interpolować wszystkie wartości dołączone do piksela (przynajmniej ich pozycje), a shader pikseli zostanie zastosowany do każdego piksela zawartego w trójkącie 2D w celu wygeneruj kolor dla każdego piksela:

(Wyświetl dużą wersję)

Ten proces jest wykonywany dla każdej ściany zdefiniowanej przez bufor indeksu.

Oczywiście, ze względu na swoją równoległą naturę, GPU jest w stanie przetworzyć ten krok dla wielu twarzy jednocześnie i osiągnąć naprawdę dobrą wydajność.

GLSL

Właśnie widzieliśmy, że do renderowania trójkątów GPU potrzebuje dwóch shaderów: Vertex Shader i Pixel Shader. Te moduły cieniujące są napisane w języku o nazwie Graphics Library Shader Language (GLSL). Wygląda jak C.

Oto przykład popularnego programu do cieniowania wierzchołków:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Vertex Shader Struktura

Vertex Shader zawiera następujące elementy:

  • Atrybuty . Atrybut definiuje część wierzchołka. Domyślnie wierzchołek powinien zawierać przynajmniej pozycję ( vector3:x, y, z ). Jednak jako programista możesz zdecydować się na dodanie większej ilości informacji. Na przykład w poprzednim module cieniującym istnieje vector2 o nazwie uv (tj. współrzędne tekstury, które pozwalają zastosować teksturę 2D do obiektu 3D).
  • Mundury . Uniform to zmienna używana przez moduł cieniujący i definiowana przez procesor. Jedyny uniform, jaki mamy tutaj, to macierz używana do rzutowania położenia wierzchołka (x, y, z) na ekran (x, y).
  • Różne . Zmienne zmienne to wartości tworzone przez Vertex Shader i przesyłane do Pixel Shadera. W tym przypadku Vertex Shader prześle wartość vUV (prostą kopię uv ) do Pixel Shadera. Oznacza to, że piksel jest tutaj zdefiniowany ze współrzędnymi położenia i tekstury. Te wartości będą interpolowane przez GPU i wykorzystane przez pixel shader.
  • Główny . Funkcja o nazwie main jest kodem wykonywanym przez GPU dla każdego wierzchołka i musi dawać przynajmniej wartość dla gl_position (pozycja bieżącego wierzchołka na ekranie).

W naszym przykładzie widzimy, że Vertex Shader jest dość prosty. Generuje zmienną systemową (zaczynającą się od gl_ ) o nazwie gl_position w celu zdefiniowania położenia skojarzonego piksela i ustawia zmienną zmienną o nazwie vUV .

Voodoo za matrycami

Rzecz w naszym shaderze polega na tym, że mamy macierz o nazwie worldViewProjection i używamy tej macierzy do rzutowania pozycji wierzchołka na zmienną gl_position . Fajnie, ale jak uzyskać wartość tej matrycy? Jest to uniform, więc musimy go zdefiniować po stronie CPU (za pomocą JavaScript).

To jedna ze złożonych części robienia 3D. Musisz rozumieć złożoną matematykę (lub będziesz musiał użyć silnika 3D, takiego jak Babylon.js, o czym zobaczymy później).

Macierz worldViewProjection to połączenie trzech różnych macierzy:

(Wyświetl dużą wersję)

Wykorzystanie powstałej matrycy umożliwia nam przekształcenie wierzchołków 3D do pikseli 2D, z uwzględnieniem punktu widzenia i wszystkiego, co dotyczy położenia, skali i obrotu bieżącego obiektu.

Twoim obowiązkiem jako programisty 3D jest stworzenie i aktualizowanie tej matrycy.

Powrót do shaderów

Gdy Vertex Shader zostanie wykonany na każdym wierzchołku (wtedy trzy razy), otrzymamy trzy piksele z prawidłową gl_position i wartością vUV . GPU będzie interpolować te wartości na każdym pikselu zawartym w trójkącie utworzonym z tych pikseli.

Następnie dla każdego piksela wykona pixel shader:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

Struktura cieniowania pikseli (lub fragmentów)

Struktura Pixel Shadera jest podobna do Vertex Shadera:

  • Różne . Zmienne zmienne to wartości tworzone przez Vertex Shader i przesyłane do Pixel Shadera. W tym przypadku Pixel Shader otrzyma wartość vUV z Vertex Shadera.
  • Mundury . Uniform to zmienna używana przez moduł cieniujący i definiowana przez procesor. Jedynym uniformem jaki mamy tutaj jest sampler, czyli narzędzie służące do odczytywania kolorów tekstur.
  • Główny . Funkcja o nazwie main to kod wykonywany przez GPU dla każdego piksela, który musi dawać przynajmniej wartość gl_FragColor (tj. kolor bieżącego piksela).

Ten pixel shader jest dość prosty: odczytuje kolor z tekstury za pomocą współrzędnych tekstury z Vertex Shadera (który z kolei pobiera go z wierzchołka).

Problem polega na tym, że kiedy tworzone są shadery, jesteś dopiero w połowie drogi, ponieważ wtedy musisz mieć do czynienia z dużą ilością kodu WebGL. Rzeczywiście, WebGL jest naprawdę potężny, ale także bardzo niskopoziomowy i musisz zrobić wszystko sam, od tworzenia buforów po definiowanie struktur wierzchołków. Musisz także wykonać całą matematykę, ustawić wszystkie stany, obsłużyć ładowanie tekstur i tak dalej.

Zbyt trudne? BABYLON.ShaderMateriał na ratunek

Wiem, o czym myślisz: „Shadery są naprawdę fajne, ale nie chcę zawracać sobie głowy wewnętrzną hydrauliką WebGL ani nawet matematyką”.

I masz rację! To jest całkowicie uzasadnione pytanie i właśnie dlatego stworzyłem Babylon.js!

Aby korzystać z Babylon.js, potrzebujesz najpierw prostej strony internetowej:

 <!DOCTYPE html> <html> <head> <title>Babylon.js</title> <script src="Babylon.js"></script> <script type="application/vertexShader"> precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Normal varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; } </script> <script type="application/fragmentShader"> precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); } </script> <script src="index.js"></script> <style> html, body { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; margin: 0px; overflow: hidden; } #renderCanvas { width: 100%; height: 100%; touch-action: none; -ms-touch-action: none; } </style> </head> <body> <canvas></canvas> </body> </html>

Zauważysz, że moduły cieniujące są definiowane przez <script> . Dzięki Babylon.js możesz je również zdefiniować w osobnych plikach (plikach .fx ).

  • Źródło Babylon.js
  • Repozytorium GitHub

Wreszcie główny kod JavaScript jest następujący:

 "use strict"; document.addEventListener("DOMContentLoaded", startGame, false); function startGame() { if (BABYLON.Engine.isSupported()) { var canvas = document.getElementById("renderCanvas"); var engine = new BABYLON.Engine(canvas, false); var scene = new BABYLON.Scene(engine); var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene); camera.attachControl(canvas); // Creating sphere var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene); var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] }); amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene)); sphere.material = amigaMaterial; engine.runRenderLoop(function () { sphere.rotation.y += 0.05; scene.render(); }); } };

Widać, że używam BABYLON.ShaderMaterial , aby pozbyć się ciężaru kompilacji, łączenia i obsługi shaderów.

Kiedy tworzysz BABYLON.ShaderMaterial , musisz określić element DOM używany do przechowywania shaderów lub podstawową nazwę plików, w których znajdują się shadery. Jeśli zdecydujesz się na użycie plików, musisz utworzyć plik dla każdego modułu cieniującego i użyć następującego wzorca: basename.vertex.fx i basename.fragment.fx . Następnie będziesz musiał stworzyć taki materiał:

 var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });

Musisz także podać nazwy atrybutów i mundurów, których używasz.

Następnie możesz bezpośrednio ustawić wartości swoich uniformów i próbników za pomocą funkcji setTexture , setFloat , setFloats , setColor3 , setColor4 , setVector2 , setVector3 , setVector4 , setMatrix .

Całkiem proste, prawda?

A czy pamiętasz poprzednią macierz worldViewProjection , używającą Babylon.js i BABYLON.ShaderMaterial . Po prostu nie musisz się tym martwić! BABYLON.ShaderMaterial automatycznie obliczy to za Ciebie, ponieważ zadeklarujesz to na liście mundurów.

BABYLON.ShaderMaterial może również obsłużyć dla Ciebie następujące matryce:

  • world ,
  • view ,
  • projection ,
  • worldView ,
  • worldViewProjection .

Nie ma już potrzeby matematyki. Na przykład za każdym razem, gdy wykonujesz sphere.rotation.y += 0.05 , macierz world sfery zostanie wygenerowana dla Ciebie i przesłana do GPU.

Zobacz wynik na żywo dla siebie.

Stwórz swój własny shader (CYOS)

Teraz chodźmy bardziej i stwórzmy stronę, na której możesz dynamicznie tworzyć własne shadery i natychmiast zobaczyć wynik. Ta strona będzie używać tego samego kodu, który omówiliśmy wcześniej, oraz obiektu BABYLON.ShaderMaterial do kompilacji i wykonania shaderów, które utworzysz.

Użyłem edytora kodu ACE dla Create Your Own Shader (CYOS). To niesamowity edytor kodu z podświetlaniem składni. Zapraszam do obejrzenia.

Korzystając z pierwszego pola kombi, będziesz mógł wybrać predefiniowane shadery. Każdego zobaczymy zaraz potem.

Możesz także zmienić siatkę (tj. obiekt 3D) używaną do podglądu twoich shaderów za pomocą drugiego pola kombi.

Przycisk kompilacji służy do tworzenia nowego BABYLON.ShaderMaterial z twoich shaderów. Kod używany przez ten przycisk jest następujący:

 // Compile shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, { vertexElement: "vertexShaderCode", fragmentElement: "fragmentShaderCode", }, { attributes: ["position", "normal", "uv"], uniforms: ["world", "worldView", "worldViewProjection"] }); var refTexture = new BABYLON.Texture("ref.jpg", scene); refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE; refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE; var amigaTexture = new BABYLON.Texture("amiga.jpg", scene); shaderMaterial.setTexture("textureSampler", amigaTexture); shaderMaterial.setTexture("refSampler", refTexture); shaderMaterial.setFloat("time", 0); shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero()); shaderMaterial.backFaceCulling = false; mesh.material = shaderMaterial;

Niesamowicie proste, prawda? Materiał jest gotowy do wysłania trzech wstępnie obliczonych macierzy ( world , worldView i worldViewProjection ). Wierzchołki będą miały współrzędne pozycji, normalnej i tekstury. Wczytane są również dwie tekstury:

amiga.jpg (Wyświetl dużą wersję)
ref.jpg (Wyświetl dużą wersję)

Wreszcie, renderLoop to miejsce, w którym aktualizuję dwa wygodne uniformy:

  • Jeden nazywa się time i dostaje kilka zabawnych animacji.
  • Drugi nazywa się cameraPosition , który pobiera pozycję kamery do shaderów (przydatne w równaniach oświetlenia).

 engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });

Podstawowy Shader

Zacznijmy od pierwszego shadera zdefiniowanego w CYOS: podstawowego shadera.

Znamy już ten shader. Oblicza gl_position i używa współrzędnych tekstury do pobrania koloru dla każdego piksela.

Aby obliczyć pozycję piksela, potrzebujemy tylko macierzy worldViewProjection i pozycji wierzchołka:

 precision highp float; // Attributes attribute vec3 position; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Współrzędne tekstury ( uv ) są przesyłane w niezmienionej postaci do modułu pixel shader.

Pamiętaj, że musimy dodać precision mediump float w pierwszym wierszu zarówno dla Vertex, jak i Pixel Shader, ponieważ Chrome tego wymaga. Określa, że ​​dla lepszej wydajności nie używamy wartości zmiennoprzecinkowych o pełnej precyzji.

Pixel Shader jest jeszcze prostszy, ponieważ wystarczy użyć współrzędnych tekstury i pobrać kolor tekstury:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }

Widzieliśmy wcześniej, że uniform textureSampler jest wypełniony teksturą amiga . Wynik jest więc następujący:

(Wyświetl dużą wersję)

Czarno-biały cieniowanie

Kontynuujmy nowy shader: czarno-biały shader. Celem tego modułu cieniującego jest użycie poprzedniego, ale z trybem renderowania tylko w czerni i bieli.

Aby to zrobić, możemy zachować ten sam Vertex Shader. Pixel Shader zostanie nieznacznie zmodyfikowany.

Pierwsza opcja, jaką mamy, to wziąć tylko jeden składnik, taki jak zielony:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }

Jak widać, zamiast .rgb (ta operacja nazywa się swizzle), użyliśmy .ggg .

Ale jeśli zależy nam na naprawdę dokładnym efekcie czarno-białym, to obliczenie luminancji (uwzględniającej wszystkie składowe) byłoby lepsze:

 precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11)); gl_FragColor = vec4(luminance, luminance, luminance, 1.0); }

Operacja dot (lub iloczyn dot ) jest obliczana w następujący sposób: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z .

Tak więc w naszym przypadku luminance = r * 0.3 + g * 0.59 + b * 0.11 . (Te wartości są oparte na fakcie, że ludzkie oko jest bardziej wrażliwe na zieleń.)

Brzmi fajnie, prawda?

(Wyświetl dużą wersję)

Cieniowanie komórek

Przejdźmy do bardziej złożonego shadera: shadera komórek.

Ten będzie wymagał od nas pobrania normalnych wierzchołków i pozycji wierzchołka w pixel shaderze. Tak więc Vertex Shader będzie wyglądał tak:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 world; uniform mat4 worldViewProjection; // Varying varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vPositionW = vec3(world * vec4(position, 1.0)); vNormalW = normalize(vec3(world * vec4(normal, 0.0))); vUV = uv; }

Pamiętaj, że używamy również macierzy świata, ponieważ pozycja i normalna są przechowywane bez żadnej transformacji i musimy zastosować macierz świata, aby uwzględnić rotację obiektu.

Pixel Shader jest następujący:

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { float ToonThresholds[4]; ToonThresholds[0] = 0.95; ToonThresholds[1] = 0.5; ToonThresholds[2] = 0.2; ToonThresholds[3] = 0.03; float ToonBrightnessLevels[5]; ToonBrightnessLevels[0] = 1.0; ToonBrightnessLevels[1] = 0.8; ToonBrightnessLevels[2] = 0.6; ToonBrightnessLevels[3] = 0.35; ToonBrightnessLevels[4] = 0.2; vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); vec3 color = texture2D(textureSampler, vUV).rgb; if (ndl > ToonThresholds[0]) { color *= ToonBrightnessLevels[0]; } else if (ndl > ToonThresholds[1]) { color *= ToonBrightnessLevels[1]; } else if (ndl > ToonThresholds[2]) { color *= ToonBrightnessLevels[2]; } else if (ndl > ToonThresholds[3]) { color *= ToonBrightnessLevels[3]; } else { color *= ToonBrightnessLevels[4]; } gl_FragColor = vec4(color, 1.); }

Celem tego shadera jest symulacja światła, a zamiast obliczania płynnego cieniowania, zastosujemy światło zgodnie z określonymi progami jasności. Na przykład, jeśli natężenie światła wynosi od 1 (maksimum) do 0,95, kolor obiektu (pobranego z tekstury) zostanie zastosowany bezpośrednio. Jeśli intensywność wynosi od 0,95 do 0,5, kolor zostanie osłabiony o współczynnik 0,8. I tak dalej.

Ten moduł cieniujący składa się głównie z czterech kroków.

Najpierw deklarujemy progi i stałe poziomów.

Następnie obliczamy oświetlenie za pomocą równania Phonga (rozważymy, że światło się nie porusza):

 vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));

Intensywność światła na piksel zależy od kąta między normalnym a kierunkiem światła.

Następnie otrzymujemy kolor tekstury dla piksela.

Na koniec sprawdzamy próg i stosujemy poziom do koloru.

Wynik wygląda jak obiekt z kreskówek:

(Wyświetl dużą wersję)

Phong Shader

W poprzednim shaderze użyliśmy części równania Phonga. Wykorzystajmy to teraz całkowicie.

Vertex Shader jest tutaj wyraźnie prosty, ponieważ wszystko zostanie zrobione w pixel shaderze:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec4 outPosition = worldViewProjection * vec4(position, 1.0); gl_Position = outPosition; vUV = uv; vPosition = position; vNormal = normal; }

Zgodnie z równaniem musimy obliczyć części „rozproszoną” i „odbłyskową”, używając kierunku światła i normalnej wierzchołka:

 precision highp float; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; // Uniforms uniform mat4 world; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 vLightPosition = vec3(0, 20, 10); // World values vec3 vPositionW = vec3(world * vec4(vPosition, 1.0)); vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0))); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); vec3 color = texture2D(textureSampler, vUV).rgb; // diffuse float ndl = max(0., dot(vNormalW, lightVectorW)); // Specular vec3 angleW = normalize(viewDirectionW + lightVectorW); float specComp = max(0., dot(vNormalW, angleW)); specComp = pow(specComp, max(1., 64.)) * 2.; gl_FragColor = vec4(color * ndl + vec3(specComp), 1.); }

Użyliśmy już części diffuse w poprzednim shaderze, więc tutaj wystarczy dodać część specular. Więcej informacji na temat cieniowania Phong można znaleźć w Wikipedii.

Wynik naszej sfery:

(Wyświetl dużą wersję)

Odrzuć shader

W przypadku shadera odrzucania chciałbym wprowadzić nową koncepcję: słowo kluczowe discard .

Ten moduł cieniujący odrzuca każdy piksel inny niż czerwony i tworzy iluzję wykopanego obiektu.

Vertex Shader jest taki sam, jak w podstawowym shaderze:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; // Varying varying vec2 vUV; void main(void) { gl_Position = worldViewProjection * vec4(position, 1.0); vUV = uv; }

Pixel Shader po swojej stronie będzie musiał przetestować kolor i użyć opcji odrzucenia, gdy na przykład zielony składnik jest zbyt wysoki:

 precision highp float; varying vec2 vUV; // Refs uniform sampler2D textureSampler; void main(void) { vec3 color = texture2D(textureSampler, vUV).rgb; if (color.g > 0.5) { discard; } gl_FragColor = vec4(color, 1.); }

Wynik jest trochę zabawny:

(Wyświetl dużą wersję)

Shader fali

Dużo bawiliśmy się shaderem pikseli, ale chcę również poinformować, że możemy zrobić wiele rzeczy z Vertex Shaderami.

W przypadku modułu do cieniowania fali ponownie użyjemy modułu do cieniowania pikseli Phong.

Vertex Shader użyje jednolitego nazwanego time , aby uzyskać kilka animowanych wartości. Używając tego uniformu, shader wygeneruje falę z pozycjami wierzchołków:

 precision highp float; // Attributes attribute vec3 position; attribute vec3 normal; attribute vec2 uv; // Uniforms uniform mat4 worldViewProjection; uniform float time; // Varying varying vec3 vPosition; varying vec3 vNormal; varying vec2 vUV; void main(void) { vec3 v = position; vx += sin(2.0 * position.y + (time)) * 0.5; gl_Position = worldViewProjection * vec4(v, 1.0); vPosition = position; vNormal = normal; vUV = uv; }

Sinus jest stosowany do position.y , a wynik jest następujący:

(Wyświetl dużą wersję)

Mapowanie sferyczne środowiska

Ten został w dużej mierze zainspirowany artykułem „Tworzenie cieniowania odbić sferycznych/środowiska”. Pozwolę ci przeczytać ten doskonały artykuł i pobawić się powiązanym shaderem.

(Wyświetl dużą wersję)

Shader Fresnela

Chciałbym zakończyć ten artykuł moim ulubionym: shaderem Fresnela.

Ten moduł cieniujący służy do zastosowania różnej intensywności w zależności od kąta między kierunkiem widoku a normalną wierzchołka.

Vertex Shader to ten sam, który jest używany przez Cell-shading Shader i możemy łatwo obliczyć wyraz Fresnela w naszym Pixel Shaderze (ponieważ mamy normalną i pozycję kamery, które można wykorzystać do oceny kierunku widoku):

 precision highp float; // Lights varying vec3 vPositionW; varying vec3 vNormalW; // Refs uniform vec3 cameraPosition; uniform sampler2D textureSampler; void main(void) { vec3 color = vec3(1., 1., 1.); vec3 viewDirectionW = normalize(cameraPosition - vPositionW); // Fresnel float fresnelTerm = dot(viewDirectionW, vNormalW); fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.); gl_FragColor = vec4(color * fresnelTerm, 1.); } 
(Wyświetl dużą wersję)

Twój Shader?

Jesteś teraz lepiej przygotowany do tworzenia własnego shadera. Zapraszam do publikowania postów na forum Babylon.js, aby dzielić się swoimi eksperymentami!

Jeśli chcesz pójść dalej, oto kilka przydatnych linków:

  • Babylon.js, oficjalna strona internetowa
  • Babylon.js, repozytorium GitHub
  • Forum Babylon.js, twórcy gier HTML5
  • Stwórz własny Shader (CYOS), Babylon.js
  • Język cieniowania OpenGL”, Wikipedia
  • Język cieniowania OpenGL, dokumentacja