In de keynote voor Dag 2 van // Build 2014 (zie 2: 24-2: 28) demonstreerden Microsoft-evangelisten Steven Guggenheimer en John Shewchuk hoe Oculus Rift-ondersteuning werd toegevoegd aan Babylon.js. En een van de belangrijkste dingen voor deze demo was het werk dat we hebben gedaan op een specifieke shader om lenzen te simuleren, zoals je op deze foto kunt zien:
Ik presenteerde ook een sessie met Frank Olivier en Ben Constable over afbeeldingen op IE en Babylon.js.
Dit leidt me naar een van de vragen die mensen me vaak stellen over Babylon.js: "Wat bedoel je met shaders?"Dus in dit bericht ga ik je uitleggen hoe shaders werken en enkele voorbeelden geven van veelvoorkomende soorten shaders.
Voordat we beginnen met experimenteren, moeten we eerst zien hoe dingen intern werken.
Bij hardware-versnelde 3D bespreken we twee CPU's: de hoofd-CPU en de GPU. De GPU is een soort uiterst gespecialiseerde CPU.
De GPU is een toestandsmachine die u instelt met behulp van de CPU. De CPU zal bijvoorbeeld de GPU configureren om lijnen te renderen in plaats van driehoeken. Of het bepaalt dat transparantie aan is, enzovoort.
Nadat alle statussen zijn ingesteld, definieert de CPU wat moet worden weergegeven: de geometrie, die is samengesteld uit een lijst met punten (de hoekpunten en opgeslagen in een array genaamd vertex buffer) en een lijst met indexen (de vlakken of driehoeken worden opgeslagen in een array met de naam index buffer).
De laatste stap voor de CPU is om te definiëren hoe de geometrie moet worden gerenderd en voor deze specifieke taak zal de CPU bepalen shaders voor de GPU. Shaders zijn een stuk code dat de GPU zal uitvoeren voor elk van de hoekpunten en pixels die het moet renderen.
Eerst wat woordenschat: denk aan een vertex (hoekpunten als er meerdere zijn) als een "punt" in een 3D-omgeving (in tegenstelling tot een punt in een 2D-omgeving).
Er zijn twee soorten shaders: vertex shaders en pixel (of fragment) shaders.
Voordat we in de schaduw graven, laten we een stap terug doen. Om pixels te renderen, neemt de GPU de geometrie aan die door de CPU is gedefinieerd en doet hij het volgende:
Met behulp van de indexbuffer worden drie hoekpunten verzameld om een driehoek te definiëren: de indexbuffer bevat een lijst met vertex-indexen. Dit betekent dat elke vermelding in de indexbuffer het nummer is van een hoekpunt in de hoekpuntbuffer. Dit is echt handig om dubbele hoekpunten te vermijden.
De volgende indexbuffer is bijvoorbeeld een lijst met twee vlakken: [1 2 3 1 3 4]
. Het eerste vlak bevat vertex 1, vertex 2 en vertex 3. Het tweede vlak bevat vertex 1, vertex 3 en vertex 4. Er zijn dus vier hoekpunten in deze geometrie:
De hoekpuntshader wordt toegepast op elke hoek van de driehoek. Het primaire doel van de hoekpuntshader is om een pixel te produceren voor elk hoekpunt (de projectie op het 2D-scherm van de 3D-vertex):
Met behulp van deze drie pixels (die een 2D-driehoek op het scherm definiëren) interpoleert de GPU alle waarden die aan de pixel zijn bevestigd (ten minste de positie) en wordt de pixel-arcering toegepast op elke pixel in de 2D-driehoek om genereer een kleur voor elke pixel:
Dit proces wordt uitgevoerd voor elk vlak dat wordt gedefinieerd door de indexbuffer.
Het is duidelijk dat, vanwege zijn parallelle aard, de GPU in staat is om deze stap voor veel gezichten gelijktijdig te verwerken en daardoor echt goede prestaties te behalen.
We hebben zojuist gezien dat voor het renderen van driehoeken, de GPU twee shaders nodig heeft: de vertex-arcering en de pixel-arcering. Deze shaders zijn geschreven met behulp van de taal GLSL (Graphics Library Shader Language). Het lijkt op C.
Voor Internet Explorer 11 hebben we een compiler ontwikkeld om GLSL te transformeren naar HLSL (High Level Shader Language), de shadertaal van DirectX 11. Hiermee kan IE11 ervoor zorgen dat de shadercode veilig is (je wilt niet gebruiken WebGL om uw computer opnieuw in te stellen!):
Hier is een voorbeeld van een gemeenschappelijke vertex-arcering:
precisie highp vlotter; // Attributenattribuut vec3-positie; kenmerk vec2 uv; // Uniformen uniforme mat4 worldViewProjection; // Variërend variërende vec2 vUV; void main (void) gl_Position = worldViewProjection * vec4 (position, 1.0); vUV = uv;
Een vertex-arcering bevat de volgende:
vector3: x, y, z
). Maar als ontwikkelaar kun je besluiten om meer informatie toe te voegen. In de vorige shader is er bijvoorbeeld een vector2
genaamd uv
(textuurcoördinaten waarmee we een 2D-structuur op een 3D-object kunnen toepassen).(x, y, z)
naar het scherm (x, y)
.VUV
(een eenvoudige kopie van uv
) waarde voor de pixelschaduw. Dit betekent dat hier een pixel wordt gedefinieerd met een positie- en textuurcoördinaten. Deze waarden worden geïnterpoleerd door de GPU en gebruikt door de pixelschaduw. hoofd()
is de code die door de GPU wordt uitgevoerd voor elke vertex en moet op zijn minst een waarde voor produceren gl_position
(de positie op het scherm van de huidige vertex). We kunnen in onze sample zien dat de vertex-shader vrij eenvoudig is. Het genereert een systeemvariabele (te beginnen met gl_
) genaamd gl_position
om de positie van de bijbehorende pixel te definiëren en wordt een variabele variabele ingesteld die wordt genoemd VUV
.
In onze arcering hebben we een matrix met de naam worldViewProjection
. We gebruiken deze matrix om de vertex-positie naar de projectie te projecteren gl_position
variabel. Dat is cool, maar hoe krijgen we de waarde van deze matrix? Het is een uniform, dus we moeten het definiëren aan de CPU-kant (met behulp van JavaScript).
Dit is een van de complexe onderdelen van het doen van 3D. Je moet complexe wiskunde begrijpen (of je zult een 3D-engine moeten gebruiken, zoals Babylon.js, die we later zullen zien).
De worldViewProjection
matrix is de combinatie van drie verschillende matrices:
Met behulp van de resulterende matrix kunnen we 3D-hoekpunten transformeren in 2D-pixels, rekening houdend met het gezichtspunt en alles met betrekking tot de positie / schaal / rotatie van het huidige object.
Dit is uw verantwoordelijkheid als 3D-ontwikkelaar: om deze matrix up-to-date te maken en te houden.
Zodra de vertex-arcering op elk hoekpunt wordt uitgevoerd (driemaal, toen), hebben we drie pixels met een correcte waarde gl_position
en een VUV
waarde. De GPU interpoleert deze waarden vervolgens op elke pixel in de driehoek die door deze pixels wordt geproduceerd.
Vervolgens voert het voor elke pixel de pixel shader uit:
precisie highp vlotter; variërende vec2 vUV; uniforme sampler2D textureSampler; void main (void) gl_FragColor = texture2D (textureSampler, vUV);
De structuur van een pixel-shader is vergelijkbaar met een hoekpunt-arcering:
VUV
waarde van de vertex-arcering. hoofd
is de code die door de GPU voor elke pixel wordt uitgevoerd en moet op zijn minst een waarde voor produceren gl_FragColor
(de kleur van de huidige pixel). Deze pixel shader is vrij eenvoudig: hij leest de kleur uit de textuur met textuurcoördinaten van de vertex-arcering (die deze op zijn beurt uit de top heeft gehaald).
Wilt u het resultaat van zo'n shader zien? Hier is het:
Dit wordt in realtime weergegeven; je kunt de bol met je muis slepen.Om dit resultaat te bereiken, zult u te maken krijgen met een lot van WebGL-code. Inderdaad, WebGL is een echt krachtige maar echt low-level API, en je moet alles zelf doen, van het maken van de buffers tot het definiëren van vertex-structuren. Je moet ook alle wiskunde doen en alle toestanden instellen en de textuur laden en zo verder ...
Ik weet wat je denkt: shaders zijn echt gaaf, maar ik wil geen moeite doen met WebGL intern loodgieterswerk of zelfs met wiskunde.
En dat is prima! Dit is een volkomen legitieme vraag, en dat is precies waarom ik Babylon.js heb gemaakt.
Laat me je de code voorstellen die werd gebruikt door de vorige rolling sphere demo. Allereerst heeft u een eenvoudige webpagina nodig:
Babylon.js
U zult merken dat de shaders worden gedefinieerd door >