Vertex vs Fragment Shaders in de Grafische Pipeline
Shaders zijn kleine GPU-programma’s, maar ze draaien niet allemaal op hetzelfde moment of op hetzelfde type data. De grootste vroege leersprong is begrijpen dat vertex shaders en fragment shaders verschillende problemen op verschillende frequenties oplossen. Een vertex shader draait per vertex, terwijl een fragment shader per fragment draait (ongeveer per gedekte pixelsample). Dat ene verschil verklaart het meeste gedrag, de prestatiekosten en de visuele resultaten die je in realtime rendering ziet.
Dit artikel bouwt een concreet model op van waar elke shaderfase draait, welke data die leest en welke data die produceert. De focus ligt op de scheiding tussen vertex en fragment, en daarna plaatsen we geometry-, tessellation- en compute-shaders in context.
Pipelinepositie en Datastroom
In een standaard rasterization-pipeline komen vertices eerst binnen, worden daarna primitieven, daarna fragmenten en tenslotte uiteindelijke framebufferpixels. De belangrijkste vraag is dus: wat krijgt elke fase als invoer?
- Invoer van vertex shader: één vertex per keer (positie, normaal, UV, tangenten, custom attributen)
- Uitvoer van vertex shader: getransformeerde clip-space-positie en varyings voor interpolatie
- Invoer van fragment shader: geïnterpoleerde varyings per fragment plus textures, uniforms en materiaalparameters
- Uitvoer van fragment shader: één of meer kleur-/dieptewaarden geschreven naar render targets
Gebruik deze stage explorer en klik op verschillende fases. Die laat zien waarom vertex en fragment altijd onderdeel zijn van het hoofdpad van graphics, terwijl sommige andere fases optioneel zijn.
Select a stage to inspect where it runs and what data it reads and writes.
Een praktische manier om dit te onthouden is bijhouden waar het aantal explosief groeit. Een mesh kan tienduizenden vertices hebben, maar een full-screen draw kan miljoenen fragmenten raken. Omdat het aantal fragmenten vaak veel groter is, kost dure wiskunde in fragment shaders meestal meer frametijd dan dezelfde wiskunde in vertex shaders.
Verantwoordelijkheden van Vertex Shaders
Een vertex shader is meestal verantwoordelijk voor geometrische plaatsing. De meest voorkomende taak is elke vertexpositie vermenigvuldigen met model-, view- en projectiematrices. Hij kan ook waarden voorbereiden voor latere fases, zoals world-space-normalen, texturecoördinaten of custom effectdata.
Wiskundig ziet de canonieke transformatie er zo uit:
Het belangrijke detail is niet de formule zelf, maar de uitvoerfrequentie. Als je model 20.000 vertices heeft, draait deze shader ongeveer 20.000 keer voor die draw call. Hij draait niet voor elke pixel op het scherm.
In de playground hieronder is de grijze driehoek de inkomende vertexdata en de blauwe driehoek de getransformeerde uitvoer na een vereenvoudigde transformatieketen. Verander rotatie, schaal en translatie en kijk hoe de uitvoer-vertexposities bewegen in normalized device space.
| rot=18deg scale=1.10 tx=0.22 ty=0.08
Wanneer deze fase verkeerd wordt begrepen, proberen beginners vaak pixelniveau-effecten in een vertex shader te doen. Dat geeft meestal blokkerige of instabiele resultaten, omdat outputs van vertex shaders alleen bekend zijn op vertices en daarna over elke driehoek geïnterpoleerd worden. Interpolatie is nuttig, maar niet gelijk aan echte per-fragment-berekening.
Verantwoordelijkheden van Fragment Shaders
Nadat primitieven zijn gerasteriseerd, genereert de GPU fragmenten. Elk fragment heeft geïnterpoleerde varyings uit de vertexfase. Nu bepaalt de fragment shader het zichtbare oppervlak: basiskleur, textuurdetail, lichtrespons, transparantielogica en soms of een fragment moet worden weggegooid.
Deze fase is waar materialen beelddetail worden. Als je een textuur samplet, normal maps combineert, BRDF-termen berekent, mist toepast en lagen blendt, gebeurt dat werk meestal hier.
In het interactieve voorbeeld ontvangt elk fragment binnen een driehoek geïnterpoleerde kleur- en UV-data. Vervolgens mengt een shaderachtig proces textuur en vertexkleur, past een eenvoudige Lambert-belichtingsterm toe en kan fragmenten onder een drempel weglaten (vergelijkbaar met alpha-cutoutlogica).
| alpha=0.55 discard=0.20
Let op wat vloeiend verandert over de driehoek: niet direct de oorspronkelijke vertexwaarden, maar geïnterpoleerde waarden. Die interpolatiestap is een van de belangrijkste redenen waarom vertex- en fragmentfases samen voorkomen. De vertexfase bereidt datapunten aan de uiteinden voor, en de fragmentfase gebruikt continue waarden daartussen om het uiteindelijke uiterlijk te berekenen.
Directe Vergelijking: Vertex vs Fragment
De snelste manier om deze fases te vergelijken is dezelfde vier vragen voor beide te stellen.
-
Hoe vaak draait het? Vertex shader: één keer per vertex. Fragment shader: één keer per fragment.
-
Wat is het hoofddoel? Vertex shader: geometrische transformatie en voorbereiding van varyings. Fragment shader: uiteindelijke shading en uitvoer van kleur/diepte.
-
Welke data domineert de invoer? Vertex shader: mesh-attributen plus transformatie-uniforms. Fragment shader: geïnterpoleerde varyings, textures, lichtbronnen en materiaal-uniforms.
-
Welk prestatiepatroon is typisch? Vertex shader: schaalt met geometriecomplexiteit. Fragment shader: schaalt met schermdekking en overdraw.
Deze verschillen impliceren praktische optimalisatieregels. Als een effect met per-vertexwiskunde en interpolatie kan worden benaderd, kan het goedkoper zijn. Als nauwkeurigheid pixelprecies moet zijn (speculaire respons, normal mapping, fijn procedureel detail), hoort het in de fragmentfase, ook als de kosten stijgen.
Veelgemaakte Fouten bij Fasekeuze
Een veelvoorkomende fout is te veel logica in fragmenten stoppen zonder naar dekking te kijken. Een full-screen post-effect op 4K kan vele miljoenen shader-aanroepen per frame uitvoeren. Een andere fout is uiterlijklogica te vroeg naar vertices verplaatsen en dan niet begrijpen waarom detail op grote driehoeken instort.
Een eenvoudig beslisproces helpt:
- Definieert deze berekening objectplaatsing? Zet het in vertex.
- Definieert deze berekening pixeluiterlijk? Zet het in fragment.
- Heeft de berekening info van naburige pixels uit al gerenderde data nodig? Dan is het vaak een latere post-process-pass, mogelijk met compute.
Dit proces is niet perfect, maar voorkomt de meeste architectuurfouten in realtime-renderingcode, vooral als je het vergelijkt met technieken zoals ray marching met signed distance fields, die volledig buiten het standaardpad van driehoeksrasterization vallen.
Andere Shadertypes in Context
Geometry Shaders
Geometry shaders draaien per primitief na de vertexfase. Ze kunnen nieuwe primitieven genereren en zijn daardoor bruikbaar voor specifieke effecten zoals layered shadow map-uitvoer of lijnuitbreiding. Toch worden ze vaak vermeden in prestatiekritische paden, omdat ze een throughput-bottleneck kunnen vormen. Veel moderne engines kiezen liever alternatieven zoals instancing, mesh shaders (op ondersteunde API’s) of compute-gedreven generatie.
Tessellation Shaders
Tessellation is opgesplitst in control- en evaluation-fases. Het onderverdeelt patch-primitieven om geometrische dichtheid op de GPU toe te voegen. Dat kan gekromde oppervlakken en displacement mapping verbeteren wanneer detail in schermruimte dat vereist. De afweging is meer complexiteit en hardware-/API-beperkingen, dus veel teams gebruiken dit selectief.
Compute Shaders
Compute shaders zijn niet gekoppeld aan rasterization. Ze voeren algemene GPU-kernels uit over threadgroepen en worden breed gebruikt voor simulatie, culling, particle-updates, voorbereiding van clustered lighting, denoising en post-processing. In moderne renderers werkt compute vaak samen met traditionele graphics-passes in plaats van ze te vervangen, en zulke passes gebruiken vaak procedurele input uit ruisfuncties zoals value noise, Perlin noise en fractale ruis.
Een nuttige mentale kaart is:
- Vertex + Fragment: kernpad van rastergraphics
- Geometry + Tessellation: optionele fases voor geometrievergroting/-verfijning
- Compute: algemeen parallel verwerkingspad dat renderingdata kan leveren of consumeren
Intuïtie Opbouwen voor Echte Projecten
Wanneer je renderproblemen debugt, bepaal dan de fasegrens waar foutieve data voor het eerst verschijnt. Als getransformeerde posities al fout zijn vóór rasterization, controleer vertexlogica. Als geometrie correct is maar kleur of belichting fout is, controleer fragmentlogica. Als topologie of onderverdeling fout is, controleer geometry-/tessellationfases. Als preprocess-buffers fout zijn, controleer compute-kernels.
Je kunt ook op fase-intentie profileren. Hoge vertexkosten volgen vaak uit dichte meshes of zware skinning. Hoge fragmentkosten volgen vaak uit grote schermdekking, dure materiaalwiskunde of overdraw door transparante lagen. Die scheiding geeft direct richting voor optimalisatie-experimenten.
Samenvatting
Vertex- en fragment-shaders verschillen omdat ze op verschillende werkeenheden draaien. Vertex shaders verwerken meshpunten en bereiden geïnterpoleerde data voor. Fragment shaders verwerken gerasteriseerde fragmenten en berekenen het uiteindelijke uiterlijk.
Als je dat uitvoeringsmodel vasthoudt, worden de meeste pipelinebeslissingen duidelijker:
- Plaatsruimte-wiskunde en voorbereiding van varyings in vertex shaders.
- Pixelnauwkeurige materiaal- en belichtingslogica in fragment shaders.
- Gebruik geometry en tessellation alleen wanneer hun specifieke mogelijkheden nodig zijn.
- Gebruik compute voor algemene GPU-taken buiten de strikte rasterflow.
Dat model schaalt van eenvoudige demo’s naar production renderers en maakt shadercode makkelijker te begrijpen, te optimaliseren en te debuggen.