Fedezze fel a forradalmi WebGL Mesh Shader pipeline-t. Tanulja meg, hogyan teszi lehetővé a Task Amplification a hatalmas, menet közbeni geometria generálást és a fejlett ritkítást a következő generációs webes grafikákhoz.
A geometria felszabadítása: Mélymerülés a WebGL Mesh Shader Task Amplification Pipeline-jába
A web már nem egy statikus, kétdimenziós médium. Egy vibráló platformmá fejlődött a gazdag, magával ragadó 3D élményekhez, a lélegzetelállító termékkonfigurátoroktól és építészeti vizualizációktól a komplex adatmodellekig és a teljes értékű játékokig. Ez az evolúció azonban példátlan követelményeket támaszt a grafikus feldolgozó egységgel (GPU) szemben. Az évek során a standard valós idejű grafikus pipeline, bár erőteljes, megmutatta a korát, gyakran szűk keresztmetszetként működött a modern alkalmazások által megkövetelt geometriai komplexitás tekintetében.
Lépjen be a Mesh Shader pipeline-ba, egy paradigmaváltó funkcióba, amely mostantól elérhető a weben a WEBGL_mesh_shader kiterjesztésen keresztül. Ez az új modell alapvetően megváltoztatja, hogyan gondolkodunk és dolgozunk fel geometriát a GPU-n. A középpontjában egy erőteljes koncepció áll: Task Amplification. Ez nem csupán egy inkrementális frissítés; ez egy forradalmi ugrás, amely a ütemezési és geometria generálási logikát a CPU-ról közvetlenül a GPU nagymértékben párhuzamos architektúrájára helyezi át, és olyan lehetőségeket szabadít fel, amelyek korábban praktikátlanok vagy lehetetlenek voltak egy webböngészőben.
Ez az átfogó útmutató egy mélymerülésre viszi Önt a mesh shader geometria pipeline-ba. Feltárjuk az architektúráját, megértjük a Task és Mesh shaderek különböző szerepeit, és feltárjuk, hogyan lehet a task amplification-t felhasználni a következő generációs, vizuálisan lenyűgöző és nagy teljesítményű webalkalmazások építéséhez.
Egy gyors visszatekintés: A hagyományos geometria pipeline korlátai
Ahhoz, hogy igazán értékelni tudjuk a mesh shaderek innovációját, először meg kell értenünk a pipeline-t, amelyet helyettesítenek. Évtizedekig a valós idejű grafikát egy viszonylag rögzített funkciójú pipeline uralta:
- Vertex Shader: Feldolgozza az egyes csúcspontokat, átalakítva őket a képernyő terébe.
- (Opcionális) Tessellation Shaders: Felosztja a geometriafoltokat a finomabb részletek létrehozásához.
- (Opcionális) Geometry Shader: Létrehozhat vagy törölhet primitíveket (pontokat, vonalakat, háromszögeket) menet közben.
- Rasterizer: Átalakítja a primitíveket pixelekké.
- Fragment Shader: Kiszámítja az egyes pixelek végső színét.
Ez a modell jól szolgált minket, de inherent korlátokkal rendelkezik, különösen, ahogy a jelenetek komplexebbé válnak:
- CPU-Bound Draw Calls: A CPU-nak az a hatalmas feladata, hogy pontosan kitalálja, mit kell rajzolni. Ez magában foglalja a frustum cullingot (a kamera látóterén kívül eső objektumok eltávolítása), az occlusion cullingot (a más objektumok által elrejtett objektumok eltávolítása) és a level-of-detail (LOD) rendszerek kezelését. Egy több millió objektumot tartalmazó jelenet esetében ez ahhoz vezethet, hogy a CPU válik az elsődleges szűk keresztmetszetté, amely nem képes elég gyorsan ellátni az éhes GPU-t.
- Rigid Input Structure: A pipeline egy merev bemeneti-feldolgozási modellre épül. Az Input Assembler egyesével táplálja a csúcspontokat, és a shaderek viszonylag korlátozott módon dolgozzák fel őket. Ez nem ideális a modern GPU architektúrákhoz, amelyek kiválóan alkalmasak a koherens, párhuzamos adatfeldolgozásra.
- Inefficient Amplification: Bár a Geometry Shaderek lehetővé tették a geometria erősítését (új háromszögek létrehozása egy bemeneti primitívből), hírhedten nem voltak hatékonyak. A kimeneti viselkedésük gyakran kiszámíthatatlan volt a hardver számára, ami teljesítményproblémákhoz vezetett, amelyek miatt sok nagyméretű alkalmazás számára nem voltak használhatók.
- Wasted Work: A hagyományos pipeline-ban, ha elküld egy háromszöget renderelésre, a vertex shader háromszor lefut, még akkor is, ha a háromszöget végül kivágják, vagy egy hátsó felületű, pixelvékony szilánk. Sok feldolgozási teljesítmény vész kárba olyan geometriára, amely semmivel sem járul hozzá a végső képhez.
A paradigmaváltás: A Mesh Shader Pipeline bemutatása
A Mesh Shader pipeline helyettesíti a Vertex, Tessellation és Geometry shader szakaszokat egy új, rugalmasabb kétszakaszos modellel:
- Task Shader (Opcionális): Egy magas szintű vezérlőszakasz, amely meghatározza, mennyi munkát kell elvégezni. Amplification Shader néven is ismert.
- Mesh Shader: A munkagép szakasz, amely adatkészleteken dolgozik, hogy kis, önálló geometria csomagokat generáljon, amelyeket "meshleteknek" neveznek.
Ez az új megközelítés alapvetően megváltoztatja a renderelési filozófiát. Ahelyett, hogy a CPU minden egyes objektumhoz minden egyes rajzolási hívást mikromenedzselne, most kiadhat egyetlen, erőteljes rajzolási parancsot, amely lényegében azt mondja a GPU-nak: "Itt van egy komplex jelenet magas szintű leírása; te találd ki a részleteket."
A GPU, a Task és Mesh shaderek segítségével, ezután ritkítást, LOD kiválasztást és procedurális generálást végezhet nagymértékben párhuzamos módon, csak a szükséges munkát indítva el a ténylegesen látható geometria generálásához. Ez a lényege a GPU-vezérelt renderelési pipeline-nak, és ez egy nagy változás a teljesítmény és a skálázhatóság szempontjából.
A Karmester: A Task (Amplification) Shader megértése
A Task Shader az új pipeline agya és a hihetetlen erejének kulcsa. Ez egy opcionális szakasz, de itt történik az "amplification". Elsődleges szerepe nem a csúcspontok vagy háromszögek generálása, hanem a munka elosztójaként való működés.
Mi az a Task Shader?
Gondoljon a Task Shaderre, mint egy hatalmas építési projekt projektmenedzserére. A CPU egy magas szintű célt ad a menedzsernek, például "építs egy városrészt". A projektmenedzser (Task Shader) maga nem rak téglákat. Ehelyett felméri az általános feladatot, ellenőrzi a terveket, és meghatározza, mely építőcsapatokra (Mesh Shader munkacsoportokra) van szükség és mennyi. Eldöntheti, hogy egy bizonyos épületre nincs szükség (ritkítás), vagy hogy egy adott terület tíz csapatot igényel, míg egy másik csak kettőt.
Technikai szempontból a Task Shader egy compute-szerű munkacsoportként fut. Hozzáférhet a memóriához, komplex számításokat végezhet, és ami a legfontosabb, eldöntheti, hány Mesh Shader munkacsoportot indítson el. Ez a döntés a hatalmának a lényege.
Az Amplification ereje
Az "amplification" kifejezés a Task Shader azon képességéből származik, hogy a saját munkacsoportjából kiindulva nulla, egy vagy több Mesh Shader munkacsoportot indítson el. Ez a képesség átalakító:
- Launch Zero: Ha a Task Shader úgy ítéli meg, hogy egy objektum vagy a jelenet egy darabja nem látható (pl. a kamera frustumán kívül), akkor egyszerűen választhatja azt, hogy nulla Mesh Shader munkacsoportot indít el. Az objektumhoz kapcsolódó összes potenciális munka eltűnik anélkül, hogy valaha is feldolgoznák tovább. Ez hihetetlenül hatékony ritkítás, amelyet teljes mértékben a GPU-n végeznek.
- Launch One: Ez egy egyszerű átmenet. A Task Shader munkacsoport úgy dönt, hogy egy Mesh Shader munkacsoportra van szükség.
- Launch Many: Itt történik a varázslat a procedurális generáláshoz. Egyetlen Task Shader munkacsoport elemezhet néhány bemeneti paramétert, és eldöntheti, hogy több ezer Mesh Shader munkacsoportot indít el. Például elindíthat egy munkacsoportot egy mező minden egyes fűszálához vagy egy sűrű klaszter minden egyes aszteroidájához, mindezt a CPU egyetlen dispatch parancsából.
Egy fogalmi pillantás a Task Shader GLSL-re
Bár a részletek bonyolulttá válhatnak, a GLSL-ben (a WebGL kiterjesztéshez) a lényeges amplification mechanizmus meglepően egyszerű. A EmitMeshTasksEXT() függvény körül forog.
Megjegyzés: Ez egy leegyszerűsített, fogalmi példa.
#version 310 es
#extension GL_EXT_mesh_shader : require
layout(local_size_x = 32, local_size_y = 1, local_size_z = 1) in;
// A CPU-ról átadott uniformok
uniform mat4 u_viewProjectionMatrix;
uniform uint u_totalObjectCount;
// Egy puffer, amely sok objektum határoló gömbjeit tartalmazza
struct BoundingSphere {
vec4 centerAndRadius;
};
layout(std430, binding = 0) readonly buffer ObjectBounds {
BoundingSphere bounds[];
} objectBounds;
void main() {
// A munkacsoport minden szála ellenőrizhet egy másik objektumot
uint objectIndex = gl_GlobalInvocationID.x;
if (objectIndex >= u_totalObjectCount) {
return;
}
// Végezzen frustum cullingot a GPU-n ehhez az objektumhoz tartozó határoló gömbhöz
BoundingSphere sphere = objectBounds.bounds[objectIndex];
bool isVisible = isSphereInFrustum(sphere.centerAndRadius, u_viewProjectionMatrix);
// Ha látható, indítson el egy Mesh Shader munkacsoportot a rajzolásához.
// Megjegyzés: Ez a logika összetettebb lehet, atomi műveletekkel számolja a látható
// objektumokat, és egy szál dispatch-elhet mindegyikükhöz.
if (isVisible) {
// Ez azt mondja a GPU-nak, hogy indítson el egy mesh taskot. A paraméterek felhasználhatók
// információk átadására a Mesh Shader munkacsoportnak.
// Az egyszerűség kedvéért elképzeljük, hogy minden task shader meghívás közvetlenül hozzárendelhető egy mesh taskhoz.
// Egy reálisabb forgatókönyv a csoportosítást és a dispatch-elést foglalja magában egyetlen szálból.
// Egy leegyszerűsített fogalmi dispatch:
// Úgy teszünk, mintha minden látható objektum saját taskot kapna, bár a valóságban
// egy task shader meghívás kezelné több mesh shader dispatch-elését.
EmitMeshTasksEXT(1u, 0u, 0u); // Ez a kulcsfontosságú amplification függvény
}
// Ha nem látható, nem csinálunk semmit! Az objektum ritkítva van, nulla GPU költséggel ezen az ellenőrzésen túl.
}
Egy valós forgatókönyvben lehet, hogy a munkacsoport egy szála összesíti az eredményeket, és egyetlen EmitMeshTasksEXT hívást indít az összes látható objektumhoz, amelyért a munkacsoport felelős.
A Személyzet: A Mesh Shader szerepe a geometria generálásában
Miután egy Task Shader elindított egy vagy több munkacsoportot, a Mesh Shader veszi át a szerepet. Ha a Task Shader a projektmenedzser, akkor a Mesh Shader a képzett építőcsapat, amely ténylegesen felépíti a geometriát.
A Munkacsoportoktól a Meshletekig
A Task Shaderhez hasonlóan a Mesh Shader is szálak együttműködő munkacsoportjaként fut. Ennek az egész munkacsoportnak a közös célja egyetlen, kis geometria köteg előállítása, amelyet meshletnek neveznek. A meshlet egyszerűen a csúcspontok és az azokat összekötő primitívek (háromszögek) gyűjteménye. Jellemzően egy meshlet kis számú csúcspontot (pl. akár 128) és háromszöget (pl. akár 256) tartalmaz, ami nagyon barátságos a modern GPU gyorsítótárak és feldolgozási modellek számára.
Ez alapvető eltérés a vertex shadertől, amelynek nem volt fogalma a szomszédairól. Egy Mesh Shaderben a munkacsoport összes szála megoszthatja a memóriát és összehangolhatja erőfeszítéseit a meshlet hatékony felépítéséhez.
Csúcspontok és primitívek generálása
Ahelyett, hogy egyetlen gl_Position-t adna vissza, egy Mesh Shader munkacsoport feltölti a kimeneti tömböket a meshlet teljes adatával. A szálak együtt dolgoznak, hogy a csúcspontok pozícióit, normálisait, UV koordinátáit és egyéb attribútumait beírják ezekbe a tömbökbe. Meghatározzák a primitíveket is azáltal, hogy megadják, mely csúcspontok alkotják az egyes háromszögeket.
A Mesh Shader utolsó lépése egy olyan függvény meghívása, mint a SetMeshOutputsEXT(), amely pontosan deklarálja, hány csúcspontot és primitívet generált. A hardver ezután átveszi ezt a meshletet, és közvetlenül a raszterizálóhoz továbbítja.
Egy fogalmi pillantás a Mesh Shader GLSL-re
Itt van egy fogalmi példa egy Mesh Shaderre, amely egy egyszerű quadot generál. Figyelje meg, hogyan működnek együtt a szálak a gl_LocalInvocationID alapján.
#version 310 es
#extension GL_EXT_mesh_shader : require
// Definiálja a meshlet maximális kimeneteit
layout(max_vertices = 4, max_primitives = 2) out;
layout(triangles) out;
layout(local_size_x = 4, local_size_y = 1, local_size_z = 1) in;
// A csúcspont adatokat ezekbe a beépített kimeneti tömbökbe írjuk
out gl_MeshVerticesEXT {
vec4 position;
vec2 uv;
} vertices[];
// A háromszög indexeket ebbe a tömbbe írjuk
out uint gl_MeshPrimitivesEXT[];
uniform mat4 u_modelViewProjectionMatrix;
void main() {
// Az ehhez a meshlethez generálandó összes csúcspont és primitív
const uint vertexCount = 4;
const uint primitiveCount = 2;
// Mondja meg a hardvernek, hogy valójában hány csúcspontot és primitívet adunk ki
SetMeshOutputsEXT(vertexCount, primitiveCount);
// Definiálja a quad csúcspont pozícióit és UV-it
vec4 positions[4] = vec4[4](
vec4(-0.5, 0.5, 0.0, 1.0),
vec4(-0.5, -0.5, 0.0, 1.0),
vec4(0.5, 0.5, 0.0, 1.0),
vec4(0.5, -0.5, 0.0, 1.0)
);
vec2 uvs[4] = vec2[4](
vec2(0.0, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 1.0),
vec2(1.0, 0.0)
);
// Hagyja, hogy a munkacsoport minden szála generáljon egy csúcspontot
uint id = gl_LocalInvocationID.x;
if (id < vertexCount) {
vertices[id].position = u_modelViewProjectionMatrix * positions[id];
vertices[id].uv = uvs[id];
}
// Hagyja, hogy az első két szál generálja a quad két háromszögét
if (id == 0) {
// Első háromszög: 0, 1, 2
gl_MeshPrimitivesEXT[0] = 0u;
gl_MeshPrimitivesEXT[1] = 1u;
gl_MeshPrimitivesEXT[2] = 2u;
}
if (id == 1) {
// Második háromszög: 1, 3, 2
gl_MeshPrimitivesEXT[3] = 1u;
gl_MeshPrimitivesEXT[4] = 3u;
gl_MeshPrimitivesEXT[5] = 2u;
}
}
Gyakorlati varázslat: A Task Amplification használati esetei
A pipeline valódi ereje akkor mutatkozik meg, amikor komplex, valós renderelési kihívásokra alkalmazzuk.
Használati eset 1: Hatalmas procedurális geometria generálás
Képzelje el, hogy egy sűrű aszteroida mezőt renderel több százezer egyedi aszteroidával. A régi pipeline-nal a CPU-nak kellett volna generálnia az egyes aszteroidák csúcspont adatait, és külön draw callt kellett volna kiadnia mindegyikhez, ami teljesen tarthatatlan megközelítés.
A Mesh Shader munkafolyamat:
- A CPU kiad egy egyetlen draw callt:
drawMeshTasksEXT(1, 1). Emellett átad néhány magas szintű paramétert, mint például a mező sugara és az aszteroida sűrűsége, egy uniform bufferben. - Egyetlen Task Shader munkacsoport fut. Beolvassa a paramétereket, és kiszámítja, hogy mondjuk 50 000 aszteroidára van szükség. Ezután meghívja az
EmitMeshTasksEXT(50000, 0, 0)függvényt. - A GPU párhuzamosan elindít 50 000 Mesh Shader munkacsoportot.
- Minden Mesh Shader munkacsoport a saját egyedi ID-jét (
gl_WorkGroupID) használja magként, hogy procedurálisan generálja az egyik egyedi aszteroida csúcspontjait és háromszögeit.
Az eredmény egy hatalmas, komplex jelenet, amely szinte teljes egészében a GPU-n generálódik, felszabadítva a CPU-t, hogy más feladatokat, például a fizikát és az AI-t kezelje.
Használati eset 2: GPU-vezérelt ritkítás nagy léptékben
Tekintsünk egy részletes városi jelenetet több millió egyedi objektummal. A CPU egyszerűen nem tudja ellenőrizni minden egyes objektum láthatóságát minden egyes képkockán.
A Mesh Shader munkafolyamat:
- A CPU feltölt egy nagy puffert, amely tartalmazza a jelenetben lévő összes objektum határoló térfogatait (pl. gömbök vagy dobozok). Ez egyszer történik meg, vagy csak akkor, ha az objektumok mozognak.
- A CPU kiad egyetlen draw callt, elindítva elegendő Task Shader munkacsoportot a határoló térfogatok teljes listájának párhuzamos feldolgozásához.
- Minden Task Shader munkacsoport hozzárendel egy darabot a határoló térfogat listából. Végig iterál a hozzárendelt objektumokon, elvégzi a frustum cullingot (és potenciálisan az occlusion cullingot) mindegyikhez, és megszámolja, hány látható.
- Végül pontosan annyi Mesh Shader munkacsoportot indít el, átadva a látható objektumok ID-it.
- Minden Mesh Shader munkacsoport megkap egy objektum ID-t, kikeresi a mesh adatait egy pufferből, és generálja a megfelelő meshleteket a rendereléshez.
Ez az egész ritkítási folyamatot a GPU-ra helyezi át, lehetővé téve olyan komplexitású jeleneteket, amelyek azonnal megbénítanának egy CPU-alapú megközelítést.
Használati eset 3: Dinamikus és hatékony Level of Detail (LOD)
Az LOD rendszerek kritikusak a teljesítmény szempontjából, egyszerűbb modellekre váltanak a távol lévő objektumok esetében. A Mesh shaderek finomabbá és hatékonyabbá teszik ezt a folyamatot.
A Mesh Shader munkafolyamat:
- Egy objektum adatai előre fel vannak dolgozva egy meshletek hierarchiájába. A durvább LOD-ok kevesebb, nagyobb meshletet használnak.
- Egy Task Shader ehhez az objektumhoz kiszámítja a távolságát a kamerától.
- A távolság alapján eldönti, melyik LOD szint megfelelő. Ezután ritkítást végezhet a meshletenként az adott LOD-hoz. Például egy nagy objektum esetében ritkíthatja az objektum hátsó oldalán lévő meshleteket, amelyek nem láthatók.
- Csak a kiválasztott LOD látható meshleteihez indítja el a Mesh Shader munkacsoportokat.
Ez lehetővé teszi a finom részletességű, menet közbeni LOD kiválasztást és ritkítást, ami sokkal hatékonyabb, mint a CPU által kicserélt teljes modellek.
Első lépések: A WEBGL_mesh_shader kiterjesztés használata
Készen áll a kísérletezésre? Íme a gyakorlati lépések a mesh shaderekkel való kezdéshez a WebGL-ben.
A támogatás ellenőrzése
Először is, ez egy élvonalbeli funkció. Ellenőriznie kell, hogy a felhasználó böngészője és hardvere támogatja-e azt.
const gl = canvas.getContext('webgl2');
const meshShaderExtension = gl.getExtension('WEBGL_mesh_shader');
if (!meshShaderExtension) {
console.error("A böngészője vagy a GPU-ja nem támogatja a WEBGL_mesh_shader-t.");
// Visszaesés egy hagyományos renderelési útvonalra
}
Az új Draw Call
Felejtse el a drawArrays és a drawElements függvényeket. Az új pipeline-t egy új parancs hívja meg. A getExtension függvényből kapott kiterjesztési objektum tartalmazza az új függvényeket.
// Indítson el 10 Task Shader munkacsoportot.
// Minden munkacsoport rendelkezik a shaderben definiált local_size-zal.
meshShaderExtension.drawMeshTasksEXT(0, 10);
A count argumentum határozza meg, hogy hány Task Shader helyi munkacsoportot kell elindítani. Ha nem használ Task Shadert, ez közvetlenül elindítja a Mesh Shader munkacsoportokat.
Shader fordítás és összekapcsolás
A folyamat hasonló a hagyományos GLSL-hez, de meshShaderExtension.MESH_SHADER_EXT és meshShaderExtension.TASK_SHADER_EXT típusú shadereket fog létrehozni. Ugyanúgy összekapcsolja őket egy programba, mint egy vertex és egy fragment shadert.
Lényeges, hogy mindkét shader GLSL forráskódjának a kiterjesztés engedélyezésére szolgáló direktívával kell kezdődnie:
#extension GL_EXT_mesh_shader : require
Teljesítménybeli szempontok és legjobb gyakorlatok
- Válassza ki a megfelelő munkacsoport méretet: A
layout(local_size_x = N)a shaderben kritikus. A 32-es vagy 64-es méret gyakran jó kiindulópont, mivel jól illeszkedik a mögöttes hardverarchitektúrákhoz, de mindig profilozza, hogy megtalálja a konkrét munkaterheléséhez optimális méretet. - Tartsa karcsún a Task Shadert: A Task Shader egy erőteljes eszköz, de egyben potenciális szűk keresztmetszet is. Az itt végzett ritkításnak és logikának a lehető leghatékonyabbnak kell lennie. Kerülje a lassú, komplex számításokat, ha előre ki lehet azokat számítani.
- Optimalizálja a meshlet méretet: Van egy hardverfüggő optimális pont a meshletenkénti csúcspontok és primitívek számához. A deklarált
max_verticesésmax_primitivesértékeket gondosan kell kiválasztani. Túl kicsi, és a munkacsoportok indításának többletköltsége dominál. Túl nagy, és elveszíti a párhuzamosságot és a gyorsítótár hatékonyságát. - Az adatok koherenciája számít: Amikor ritkítást végez a Task Shaderben, rendezze a határoló térfogat adatait a memóriában a koherens hozzáférési minták elősegítése érdekében. Ez segít a GPU gyorsítótáraknak hatékonyan működni.
- Tudja, mikor kell elkerülni őket: A Mesh shaderek nem csodaszerek. Néhány egyszerű objektum rendereléséhez a mesh pipeline többletköltsége lassabb lehet, mint a hagyományos vertex pipeline. Használja őket ott, ahol az erősségeik ragyognak: hatalmas objektumszámok, komplex procedurális generálás és GPU-vezérelt munkaterhelések.
Következtetés: A valós idejű grafika jövője a weben már most van
A Task Amplificationnel rendelkező Mesh Shader pipeline az egyik legjelentősebb előrelépést jelenti a valós idejű grafikában az elmúlt évtizedben. Azzal, hogy a paradigmát egy merev, CPU által felügyelt folyamatról egy rugalmas, GPU-vezérelt folyamatra helyezi át, lerombolja a geometriai komplexitás és a jelenet skála előtti akadályait.
Ez a technológia, amely a modern grafikus API-k, mint a Vulkan, a DirectX 12 Ultimate és a Metal irányába igazodik, már nem korlátozódik a csúcskategóriás natív alkalmazásokra. A WebGL-ben való megjelenése megnyitja az ajtót a web alapú élmények új korszakának, amelyek részletesebbek, dinamikusabbak és magával ragadóbbak, mint valaha. Azok a fejlesztők számára, akik hajlandóak elfogadni ezt az új modellt, a kreatív lehetőségek szinte korlátlanok. Az a képesség, hogy menet közben egész világokat generálhat, a legelső alkalommal, szó szerint kéznél van, közvetlenül egy webböngészőben.