Construirea de shaders cu Babylon.js
Publicat: 2022-03-10Shaders sunt un concept cheie dacă doriți să eliberați puterea brută a GPU-ului dvs. Vă voi ajuta să înțelegeți cum funcționează ele și chiar să experimentați cu puterea lor interioară într-un mod ușor, datorită Babylon.js.
Cum functioneazã?
Înainte de a experimenta, trebuie să vedem cum funcționează lucrurile pe plan intern.
Când aveți de-a face cu 3D accelerat de hardware, va trebui să aveți de-a face cu două procesoare: CPU principal și GPU. GPU-ul este un fel de CPU extrem de specializat.
Citiți suplimentare despre SmashingMag:
- Crearea unui joc WebGL multiplatformă cu Babylon.js
- Utilizarea API-ului Gamepad în jocurile web
- Introducere în modelarea poligonală și trei.js
- Cum să creezi o mașină de tobe receptivă de 8 biți
GPU-ul este o mașină de stare pe care o configurați folosind procesorul. De exemplu, procesorul va configura GPU-ul pentru a reda linii în loc de triunghiuri; va defini dacă transparența este activată; și așa mai departe.
Odată ce toate stările sunt setate, CPU poate defini ce să randeze: geometria.
Geometria este compusă din:
- o listă de puncte care sunt numite vârfuri și stocate într-o matrice numită tampon de vârfuri,
- o listă de indici care definesc fețele (sau triunghiurile) stocate într-o matrice numită index buffer.
Pasul final pentru CPU este definirea modului de redare a geometriei; pentru această sarcină, procesorul va defini shaders în GPU. Shaders sunt bucăți de cod pe care GPU-ul le va executa pentru fiecare dintre nodurile și pixelii pe care trebuie să-i redeze. (Un vârf – sau vârfuri când există mai multe dintre ele – este un „punct” în 3D).
Există două tipuri de shadere: vertex shaders și pixeli (sau fragmente).
Conducta grafică
Înainte de a pătrunde în shadere, să facem un pas înapoi. Pentru a reda pixelii, GPU-ul va lua geometria definită de CPU și va face următoarele:
- Folosind tamponul de index, trei vârfuri sunt adunate pentru a defini un triunghi.
- Bufferul de index conține o listă de indici de vârf. Aceasta înseamnă că fiecare intrare din tamponul de index este numărul unui vârf din tamponul de vârf.
- Acest lucru este cu adevărat util pentru a evita duplicarea vârfurilor.
De exemplu, următorul buffer de index este o listă de două fețe: [1 2 3 1 3 4]. Prima față conține vârful 1, vârful 2 și vârful 3. A doua față conține vârful 1, vârful 3 și vârful 4. Deci, există patru vârfuri în această geometrie:
Shaderul vertex este aplicat fiecărui vârf al triunghiului. Scopul principal al vertex shader este de a produce un pixel pentru fiecare vârf (proiecția pe ecranul 2D a vârfului 3D):
Folosind acești trei pixeli (care definesc un triunghi 2D pe ecran), GPU-ul va interpola toate valorile atașate pixelului (cel puțin pozițiile acestora), iar pixel shader-ul va fi aplicat fiecărui pixel inclus în triunghiul 2D pentru a generați o culoare pentru fiecare pixel:
Acest proces se face pentru fiecare față definită de tamponul de index.
Evident, datorită naturii sale paralele, GPU-ul este capabil să proceseze acest pas pentru o mulțime de fețe simultan și să obțină performanțe foarte bune.
GLSL
Tocmai am văzut că pentru a reda triunghiuri, GPU-ul are nevoie de două shadere: vertex shader și pixel shader. Aceste shadere sunt scrise într-o limbă numită Graphics Library Shader Language (GLSL). Se pare ca C.
Iată un eșantion de umbrire de vârf obișnuit:
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; }
Structura Vertex Shader
Un vertex shader conține următoarele:
- Atribute . Un atribut definește o porțiune a unui vârf. În mod implicit, un vârf ar trebui să conțină cel puțin o poziție (un
vector3:x, y, z
). Cu toate acestea, în calitate de dezvoltator, puteți decide să adăugați mai multe informații. De exemplu, în fostul shader, există unvector2
numituv
(adică coordonatele texturii care vă permit să aplicați o textură 2D unui obiect 3D). - Uniforme . O uniformă este o variabilă utilizată de shader și definită de CPU. Singura uniformă pe care o avem aici este o matrice folosită pentru a proiecta poziția vârfului (x, y, z) pe ecran (x, y).
- Variind . Variabilele variabile sunt valori create de vertex shader și transmise pixel shader-ului. Aici, vertex shader va transmite o
vUV
(o simplă copie auv
) către pixel shader. Aceasta înseamnă că un pixel este definit aici cu o poziție și coordonate de textură. Aceste valori vor fi interpolate de GPU și utilizate de pixel shader. - Principal . Funcția numită
main
este codul executat de GPU pentru fiecare vârf și trebuie să producă cel puțin o valoare pentrugl_position
(poziția vârfului curent pe ecran).
Putem vedea în eșantionul nostru că vertex shader este destul de simplu. Acesta generează o variabilă de sistem (începând cu gl_
) numită gl_position
pentru a defini poziția pixelului asociat și setează o variabilă variabilă numită vUV
.
Voodoo din spatele matricelor
Lucrul cu shader-ul nostru este că avem o matrice numită worldViewProjection
și folosim această matrice pentru a proiecta poziția vârfului la variabila gl_position
. Este grozav, dar cum obținem valoarea acestei matrice? Este o uniformă, așa că trebuie să o definim pe partea CPU (folosind JavaScript).
Aceasta este una dintre părțile complexe ale realizării 3D. Trebuie să înțelegeți matematica complexă (sau va trebui să utilizați un motor 3D precum Babylon.js, pe care îl vom vedea mai târziu).
Matricea worldViewProjection
este o combinație a trei matrice diferite:
Utilizarea matricei rezultate ne permite să transformăm vârfurile 3D în pixeli 2D, ținând cont de punctul de vedere și de tot ceea ce are legătură cu poziția, scara și rotația obiectului curent.
Aceasta este responsabilitatea dumneavoastră ca dezvoltator 3D: să creați și să păstrați această matrice la zi.
Înapoi la Shaders
Odată ce vertex shader este executat pe fiecare vârf (de trei ori, atunci), vom avea trei pixeli cu gl_position
corectă și o valoare vUV
. GPU-ul va interpola aceste valori pe fiecare pixel conținut în triunghiul produs cu acești pixeli.
Apoi, pentru fiecare pixel, va executa pixel shader:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Structura de umbrire a pixelilor (sau a fragmentului).
Structura unui pixel shader este similară cu cea a unui vertex shader:
- Variind . Variabilele variabile sunt valori create de vertex shader și transmise pixel shader-ului. Aici, pixel shader-ul va primi o valoare
vUV
de la vertex shader. - Uniforme . O uniformă este o variabilă utilizată de shader și definită de CPU. Singura uniformă pe care o avem aici este un sampler, care este un instrument folosit pentru a citi culorile texturii.
- Principal . Funcția numită
main
este codul executat de GPU pentru fiecare pixel și care trebuie să producă cel puțin o valoare pentrugl_FragColor
(adică culoarea pixelului curent).
Acest pixel shader este destul de simplu: citește culoarea din textură folosind coordonatele texturii din vertex shader (care, la rândul său, o primește de la vârf).
Problema este că atunci când sunt dezvoltate shadere, ești doar la jumătatea drumului, pentru că atunci trebuie să faci față cu mult cod WebGL. Într-adevăr, WebGL este cu adevărat puternic, dar și la nivel scăzut și trebuie să faci totul singur, de la crearea bufferelor până la definirea structurilor de vârf. De asemenea, trebuie să faceți toate calculele, să setați toate stările, să gestionați încărcarea texturii și așa mai departe.
Prea greu? BABYLON.ShaderMaterial pentru salvare
Știu la ce te gândești: „Shaders sunt foarte cool, dar nu vreau să mă deranjez cu instalațiile interne ale WebGL sau chiar cu matematica.”
Si ai dreptate! Aceasta este o întrebare perfect legitimă și tocmai de aceea am creat Babylon.js!
Pentru a utiliza Babylon.js, mai întâi aveți nevoie de o pagină web simplă:
<!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>
Veți observa că shaders-urile sunt definite de etichete <script>
. Cu Babylon.js, le puteți defini și în fișiere separate (fișiere .fx
).
- Sursa Babylon.js
- Depozitul GitHub
În cele din urmă, codul JavaScript principal este acesta:
"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(); }); } };
Puteți vedea că folosesc BABYLON.ShaderMaterial
pentru a scăpa de sarcina de a compila, conecta și gestiona shaders.
Când creați BABYLON.ShaderMaterial
, trebuie să specificați elementul DOM folosit pentru a stoca shaders-urile sau numele de bază al fișierelor în care se află shaders-urile. Dacă alegeți să utilizați fișiere, trebuie să creați un fișier pentru fiecare shader și să utilizați următorul model: basename.vertex.fx
și basename.fragment.fx
. Apoi, va trebui să creați materialul astfel:
var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"] });
De asemenea, trebuie să specificați numele atributelor și uniformelor pe care le utilizați.
Apoi, puteți seta direct valorile uniformelor și probelor dvs. folosind setTexture
, setFloat
, setFloats
, setColor3
, setColor4
, setVector2
, setVector3
, setVector4
, setMatrix
.
Destul de simplu, nu?
Și vă amintiți matricea anterioară worldViewProjection
, folosind Babylon.js și BABYLON.ShaderMaterial
. Doar că nu trebuie să-ți faci griji pentru asta! BABYLON.ShaderMaterial
va calcula automat pentru că îl vei declara în lista de uniforme.
BABYLON.ShaderMaterial
poate gestiona, de asemenea, următoarele matrici pentru dvs.:
-
world
, -
view
, -
projection
, -
worldView
, -
worldViewProjection
.
Nu mai e nevoie de matematică. De exemplu, de fiecare dată când executați sphere.rotation.y += 0.05
, matricea world
a sferei va fi generată pentru dvs. și transmisă la GPU.
Vezi singur rezultatul live.
Creați-vă propriul Shader (CYOS)
Acum, să mergem mai mari și să creăm o pagină în care să vă creați dinamic propriile shadere și să vedeți imediat rezultatul. Această pagină va folosi același cod pe care l-am discutat anterior și va folosi obiectul BABYLON.ShaderMaterial
pentru a compila și a executa shaders pe care le veți crea.
Am folosit editorul de cod ACE pentru Create Your Own Shader (CYOS). Este un editor de cod incredibil, cu evidențiere de sintaxă. Simțiți-vă liber să aruncați o privire la el.
Folosind prima casetă combinată, veți putea selecta shadere predefiniti. Îi vom vedea pe fiecare imediat după.
De asemenea, puteți modifica rețeaua de plasă (adică obiectul 3D) folosită pentru previzualizarea shader-urilor folosind a doua casetă combinată.
Butonul de compilare este folosit pentru a crea un nou BABYLON.ShaderMaterial
din shaderele dvs. Codul folosit de acest buton este următorul:
// 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;
Incredibil de simplu, nu? Materialul este gata să vă trimită trei matrici precalculate ( world
, worldView
și worldViewProjection
). Nodurile vor veni cu coordonatele de poziție, normal și textura. Două texturi sunt deja încărcate pentru tine:
În cele din urmă, renderLoop
este locul în care actualizez două uniforme convenabile:
- Unul se numește
time
și primește niște animații amuzante. - Celălalt se numește
cameraPosition
, care obține poziția camerei în shadere (utilă pentru ecuațiile de iluminare).
engine.runRenderLoop(function () { mesh.rotation.y += 0.001; if (shaderMaterial) { shaderMaterial.setFloat("time", time); time += 0.02; shaderMaterial.setVector3("cameraPosition", camera.position); } scene.render(); });
Shader de bază
Să începem cu primul shader definit în CYOS: shaderul de bază.
Cunoaștem deja acest shader. Acesta calculează gl_position
și folosește coordonatele texturii pentru a obține o culoare pentru fiecare pixel.
Pentru a calcula poziția pixelului, avem nevoie doar de matricea worldViewProjection
și de poziția vârfului:
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; }
Coordonatele texturii ( uv
) sunt transmise nemodificate la pixel shader.
Vă rugăm să rețineți că trebuie să adăugăm o precision mediump float
pe prima linie atât pentru vertex, cât și pentru pixel shaders, deoarece Chrome o cere. Specifică că, pentru o performanță mai bună, nu folosim valori flotante de precizie completă.
Pixel shader-ul este și mai simplu, deoarece trebuie doar să folosim coordonatele texturii și să obținem o culoare de textură:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = texture2D(textureSampler, vUV); }
Am văzut anterior că uniforma textureSampler
este umplută cu textura amiga
. Deci, rezultatul este următorul:
Shader alb-negru
Să continuăm cu un nou shader: shaderul alb-negru. Scopul acestui shader este folosirea celui precedent, dar cu un mod de randare doar alb-negru.
Pentru a face acest lucru, putem păstra același vertex shader. Pixel shader-ul va fi ușor modificat.
Prima opțiune pe care o avem este să luăm o singură componentă, cum ar fi cea verde:
precision highp float; varying vec2 vUV; uniform sampler2D textureSampler; void main(void) { gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0); }
După cum puteți vedea, în loc să folosim .rgb
(această operație se numește swizzle), am folosit .ggg
.
Dar dacă dorim un efect alb-negru cu adevărat precis, atunci calcularea luminozității (care ia în considerare toate componentele) ar fi mai bine:
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); }
Operația dot
(sau produsul dot
) se calculează astfel: result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z
.
Deci, în cazul nostru, luminance = r * 0.3 + g * 0.59 + b * 0.11
. (Aceste valori se bazează pe faptul că ochiul uman este mai sensibil la verde.)
Sună bine, nu-i așa?
Umbrire celulară
Să trecem la un shader mai complex: cel-shading shader.
Acesta ne va cere să obținem normalul vârfului și poziția vârfului în pixel shader. Deci, shaderul vertex va arăta astfel:
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; }
Vă rugăm să rețineți că folosim și matricea lumii deoarece poziția și normalul sunt stocate fără nicio transformare și trebuie să aplicăm matricea mondială pentru a ține cont de rotația obiectului.
Pixel shader-ul este după cum urmează:
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.); }
Scopul acestui shader este de a simula lumina și, în loc să calculăm umbrirea netedă, vom aplica lumina în funcție de praguri specifice de luminozitate. De exemplu, dacă intensitatea luminii este între 1 (maximum) și 0,95, culoarea obiectului (preluată din textură) ar fi aplicată direct. Dacă intensitatea este între 0,95 și 0,5, culoarea ar fi atenuată cu un factor de 0,8. Și așa mai departe.
Există în principal patru pași în acest shader.
În primul rând, declarăm pragurile și constantele nivelurilor.
Apoi, calculăm iluminarea folosind ecuația Phong (vom considera că lumina nu se mișcă):
vec3 vLightPosition = vec3(0, 20, 10); // Light vec3 lightVectorW = normalize(vLightPosition - vPositionW); // diffuse float ndl = max(0., dot(vNormalW, lightVectorW));
Intensitatea luminii pe pixel depinde de unghiul dintre direcția normală și cea a luminii.
Apoi, obținem culoarea texturii pentru pixel.
În cele din urmă, verificăm pragul și aplicăm nivelul culorii.
Rezultatul arată ca un obiect de desene animate:
Phong Shader
Am folosit o parte din ecuația Phong în shaderul anterior. Să-l folosim complet acum.
Vertex Shader este clar simplu aici, deoarece totul se va face în pixel shader:
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; }
Conform ecuației, trebuie să calculăm părțile „difuze” și „speculare” folosind direcția luminii și normala vârfurilor:
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.); }
Am folosit deja partea difuză în shaderul anterior, așa că aici trebuie doar să adăugăm partea speculară. Puteți găsi mai multe informații despre umbrirea Phong pe Wikipedia.
Rezultatul sferei noastre:
Aruncă Shader
Pentru discard shader, aș dori să introduc un nou concept: cuvântul cheie discard
.
Acest shader elimină fiecare pixel care nu este roșu și creează iluzia unui obiect săpat.
Vertex Shader este același folosit de shaderul de bază:
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-ul de pe partea sa va trebui să testeze culoarea și să folosească aruncarea atunci când, de exemplu, componenta verde este prea mare:
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.); }
Rezultatul este puțin amuzant:
Wave Shader
Ne-am jucat mult cu pixel shader, dar vreau să vă anunț și că putem face multe lucruri cu vertex shader.
Pentru wave shader, vom reutiliza pixel shader-ul Phong.
Vertex shader va folosi time
denumit uniform pentru a obține niște valori animate. Folosind această uniformă, shader-ul va genera o undă cu pozițiile vârfurilor:
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; }
Se aplică un sinus pe position.y
și rezultatul este următorul:
Cartografierea mediului sferic
Acesta a fost în mare parte inspirat de articolul „Crearea unui shader de cartografiere sferică/reflecție sferică”. Vă voi lăsa să citiți acel articol excelent și să vă jucați cu shaderul asociat.
Fresnel Shader
Aș dori să închei acest articol cu preferatul meu: shaderul Fresnel.
Acest shader este folosit pentru a aplica o intensitate diferită în funcție de unghiul dintre direcția de vizualizare și normala vârfului.
Vertex Shader este același folosit de cell-shading shader și putem calcula cu ușurință termenul Fresnel în pixel shader-ul nostru (deoarece avem poziția normală și a camerei, care poate fi folosită pentru a evalua direcția vizualizării):
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.); }
Shaderul tău?
Acum sunteți mai pregătit să vă creați propriul shader. Nu ezitați să postați pe forumul Babylon.js pentru a vă împărtăși experimentele!
Dacă vrei să mergi mai departe, iată câteva link-uri utile:
- Babylon.js, site-ul oficial
- Babylon.js, depozitul GitHub
- Forum Babylon.js, dezvoltatori de jocuri HTML5
- Creați-vă propriul Shader (CYOS), Babylon.js
- Limbajul de umbrire OpenGL”, Wikipedia
- OpenGL Shading Language, documentație