Individueel project
Doel: een clone van de Windows Media Player "Batterij" visualizer:
De batterij visualizer ondersteunt een aantal presets. Op de bovenstaande afbeelding is de 'DrowningFlower' preset te zien.
Alle presets lijken te bestaan uit het volgende:
een laag die patronen genereert
een filter die (voor of na het patroon) het beeld op een unieke manier vervormt
een filter die het tot nu toe gegenereerde beeld andere kleuren geeft
(deze stap heb ik later toegevoegd omdat het makkelijker is om een zwart-wit beeld te tekenen, en hier de kleuren van te mappen, inplaats van direct in kleur te werken en rekening te houden met doorzichtigheid en blending)
Voor elk beeld wordt het nieuwe patroon boven op het vorige beeld getekend. Dit zorgt voor een soort "visuele galm" die erg lijkt op bijv. een feedbacklus die je met een analoge videocamera zou kunnen maken (voorbeeld).
Logboek
Eerst heb ik geprobeerd de "Hello Triangle" tutorial te maken van <learnopengl.com>. Aan het einde van deze tutorial had ik een simpel programma dat reageert op window resize events, en die een fragment shader gebruikt om een enkele driehoek te tekenen in een effen kleur. Omdat het effect dat ik probeer na te maken geen 3D nodig heeft, was ik van plan alles met fragment shaders te implementeren.
Ik had de applicatie hierna nog aangepast om de driehoek
schermvullend te tekenen. Om dit te doen had ik al snel ondekt dat ik de
z-coördinaat van gl_Position
uit mijn vertex shader op 0
kon zetten, omdat de xyz coördinaten uiteindelijk door z gedeeld worden.
Op mijn NVIDIA kaart zorgde dit zoals ik verwachtte voor een
schermvullende driehoek (die oneindig groot was omdat er gedeeld wordt
door 0), maar op Intel zorgde dit voor een leeg scherm. Ik heb
uiteindelijk de z-coördinaat op 0.001 gezet, en ben hierna direct bewust
geworden van de verschillen tussen graphics drivers.
Hierna had ik de fragment shader nog wat aangepast om via uniforms de huidige tijd en schermgrotte binnen te krijgen. De schermgrotte gebruikte ik om een schermvullende kleurovergang te maken (op het groene en blauwe kleurkanaal), en de tijd waarde om een ruissignaal te maken (op het rode kleurkanaal):
Toen dit allemaal werkte vond ik het een goed moment om mijn tooling
wat te verbeteren voor het schrijven en compileren van GLSL. Ik heb
gekozen om de glslc
'compiler' te gebruiken, die GLSL omzet
naar SPIR-V.
Dit heb ik gedaan zodat ik GLSL compilatiewaarschuwingen krijg tijdens
het bouwen van mijn complete applicatie inplaats van tijdens runtime, en
zodat ik de C preprocessor kan gebruiken tijdens compileertijd, waarmee
ik geen broncode hoef te bundelen met mijn applicatie. De compiler zelf
werkt prima op zowel mijn desktop PC als mijn laptop, maar ik liep hier
gelijk weer tegen verschillen tussen de Intel en NVIDIA graphics drivers
aan.
Ik heb er een uitgebereide vraag op StackOverflow over gesteld, en ben gewezen op het feit dat de SPIR-V specificatie geen eis stelt dat drivers die SPIR-V programma's laden reflectie moeten ondersteunen. Dit zou betekenen dat ik de uniform locaties van mijn fragment shader zou moeten hard-coden, of toch GLSL gebruiken zodat ik de uniform locaties kan opvragen via hun naam als string.
Ik heb ook het build systeem iets uitgebereid om de SPIR-V binaries mee te linken met linker scripts en gegenereerde C headers om de data door te geven aan OpenGL functies. Dit was niet bijzonder spannend.
Om verder te gaan met het maken van het effect dat in de eerste afbeelding in dit document te zien is, heb ik eerst geprobeerd de spiraalvorm in Desmos na te maken. Dit was het makkelijkst te doen met een parametrische functie, wat een duur wiskunde woord is voor een functie die (in dit geval) een 2D punt geeft voor een willekeurige parameter (t). Deze functie bestaat uit het dynamische duo, de sinus en cosinus, die samen een cirkel maken. Bij deze cirkelvorm tel ik een kleinere 'snellere' cirkel op, en deze functie heeft parameters voor het aantal wikkelingen, en de grootte van de wikkelingen:
Deze functie zou ontzettend snel en eenvoudig zijn om te
implementeren op de CPU. Omdat de functie periodiek is kun je met alleen
een for
-lus genoeg punten uitrekenen om het te laten lijken
alsof je een ononderbroken kromme tekent. Helaas moet zoveel mogelijk in
dit project met OpenGL, dus heb ik deze functie als fragment shader
geïmplementeerd (binnen ShaderToy omdat het makkelijk was voor
prototypes maken). Dit is ontzettend inefficiënt, omdat je voor elke
pixel de parameter t probeert terug te rekenen, inplaats van andersom.
Hierdoor moet de functie een paar duizend keer uitgerekend worden per
pixel. Helaas valt mijn visualizer hiermee buiten de duurzaamheidsdoelen
van de vakbeschrijving.
Hierna heb ik (ook met behulp van ShaderToy) een prototype gemaakt van het video feedback effect. ShaderToy maakt het heel makkelijk om framebuffers op willekeurige inputs te zetten, en maakte het snel mogelijk om een draaiende cirkel met een feedback effect te maken.
Om dit zelfde effect in OpenGL te maken moest ik alleen handmatig een framebuffer aanmaken. Om dit te doen heb ik deels stappen gevolgd van LearnOpenGL. Ook heb ik mijn draw loop aan moeten passen om eerst de 'custom' framebuffer en eerste fragment shader te gebruiken, en daarna de output framebuffer en tweede fragment shader.
Omdat het algoritme die de spiraalvorm tekent zo traag draait heb ik uiteindelijk besloten de visualizer te tekenen op een vaste resolutie van 800x600, net als Windows Media Player. Dit zorgt ervoor dat resolutie geen invloed meer heeft op de snelheid van het effect, maar dat er wel merkbare upscaling wordt toegepast bij hogere resoluties. Dit was makkelijk te realiseren, door gewoon niks te doen met de framebuffer, en de tweede fragment shader (die nog wel op volledige resolutie draait), te laten samplen van de vaste 800x600 texture van de eerste pass.
Hierna heb ik de feedback shader code samengevoegd met die van de spiraalvorm, en daar kwam het volgende uit:
Om ervoor te zorgen dat het gerecyclede beeld weg zou krimpen, inplaats van stil blijft staan, kun je gewoon de samplecoördinaten vermenigvuldigen met een vergroottingsfactor. Dit moet alleen wel gedaan worden met (0.5, 0.5) als middenpunt, omdat de texture coördinaten genormaliseerd zijn tussen (0, 0) en (1, 1). Om het beeld te draaien gebruik je een 2D rotatiematrix, en deze kun je gewoon toe passen door de texturecoördinaten ermee te vermenigvuldigen.
Ook heb ik een paar parameters die in het Desmos voorbeeld zaten met wat willekeurig geplukte sinusfuncties laten bewegen. Dit leverde het volgende beeld op:
Naast de kleuren en het feit dat de spiraalvorm in het origineel veel kleiner is, viel mij om dat je de rand van de vorige beelden heel duidelijk kan zien in het origineel. Het beeld zelf heeft geen duidelijk zichtbare rand, dus misschien is dit een bijwerking van de graphics API die gebruikt wordt door Windows Media Player. Om dit toch na te maken heb ik met een paar if statements gewoon een rand toegevoegd aan het beeld. Hierdoor lijkt het effect er al een stuk meer op:
Om het nu helemaal af te maken heb ik het zwart-wit naar een
kleurovergang gemapt met behulp van de GLSL mix()
functie.
De grijswaarde van het beeld pak ik gewoon van een willekeurig
kleurkanaal, omdat alle kleuren toch geen tint bevatten. De kleuren van
de kleurovergang heb ik geprobeerd zo dicht mogelijk op de originele
visualizer te pakken: