OpenGL er et kraftig plattform-API som gir veldig nær tilgang til systemets maskinvare i en rekke programmeringsmiljøer.
Så hvorfor skal du bruke den?
Det gir en veldig lav prosessering for grafikk i både 2D og 3D. Generelt vil dette unngå klump som vi har på grunn av tolket eller høyt nivå programmeringsspråk. Enda viktigere, men det gir også tilgang på maskinvarenivå til en nøkkelfunksjon: GPU.
GPU kan øke hastigheten på mange applikasjoner betydelig, men den har en veldig spesifikk rolle i en datamaskin. GPU-kjerner er faktisk tregere enn CPU-kjerner. Hvis vi skulle kjøre et program som er spesielt seriell uten samtidig aktivitet, vil det nesten alltid være tregere på en GPU-kjerne enn en CPU-kjerne. Hovedforskjellen er at GPU støtter massiv parallellbehandling. Vi kan lage små programmer kalt shaders som kjører effektivt på hundrevis av kjerner samtidig. Dette betyr at vi kan ta oppgaver som ellers er utrolig repeterende og kjøre dem samtidig.
I denne artikkelen skal vi bygge en enkel Android-applikasjon som bruker OpenGL til å gjengi innholdet på skjermen. Før vi begynner, er det viktig at du allerede er kjent med kunnskap om å skrive Android-applikasjoner og syntaksen til noe C-lignende programmeringsspråk. Hele kildekoden i denne opplæringen er tilgjengelig på GitHub .
For å demonstrere kraften til OpenGL, skriver vi en relativt grunnleggende applikasjon for en Android-enhet. Nå distribueres OpenGL på Android under et delsett kalt OpenGL for innebygde systemer (OpenGL ES). Vi kan egentlig bare tenke på dette som en nedkladd versjon av OpenGL, selv om kjernefunksjonaliteten som er nødvendig fortsatt vil være tilgjengelig.
I stedet for å skrive en grunnleggende 'Hello World', vil vi skrive en bedragerisk enkel applikasjon: en Mandelbrot-generator. De Mandelbrot sett er basert i feltet komplekse tall . Kompleks analyse er et vakkert stort felt, så vi vil fokusere på det visuelle resultatet mer enn den faktiske matematikken som ligger bak.
Med OpenGL er det lettere å bygge en Mandelbrot-settgenerator enn du tror! kvitring
Når vi lager applikasjonen, vil vi sørge for at den bare distribueres til de med riktig OpenGL-støtte. Start med å erklære bruken av OpenGL 2.0 i manifestfilen, mellom manifesterklæringen og applikasjonen:
MainActivity
På dette punktet er støtte for OpenGL 2.0 allestedsnærværende. OpenGL 3.0 og 3.1 får stadig større kompatibilitet, men å skrive for begge vil utelate omtrent 65% av enhetene , så ta bare avgjørelsen hvis du er sikker på at du trenger ekstra funksjonalitet. De kan implementeres ved å sette versjonen til henholdsvis '0x000300000' og '0x000300001'.
Når du lager denne OpenGL-applikasjonen på Android, har du vanligvis tre hovedklasser som brukes til å tegne overflaten: din GLSurfaceView
, en utvidelse av GLSurfaceView.Renderer
, og en implementering av en MainActivity
. Derfra vil vi lage forskjellige modeller som vil kapsle inn tegninger.
FractalGenerator
, kalt GLSurfaceView
i dette eksemplet, kommer egentlig bare til å instantiere public class FractalGenerator extends Activity { private GLSurfaceView mGLView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Create and set GLSurfaceView mGLView = new FractalSurfaceView(this); setContentView(mGLView); } //[...] @Override protected void onPause() { super.onPause(); mGLView.onPause(); } @Override protected void onResume() { super.onResume(); mGLView.onResume(); } }
og rute eventuelle globale endringer nedover linjen. Her er et eksempel som i det vesentlige kommer til å være kjeleplatekoden din:
GLSurfaceView
Dette kommer også til å være klassen der du vil sette andre modifikatorer for aktivitetsnivå, (for eksempel oppslukende fullskjerm ).
En klasse dypere har vi en utvidelse av setEGLContextClientVersion(int version)
, som vil fungere som vårt primære syn. I denne klassen setter vi versjonen, setter opp en gjengir og kontrollerer berøringshendelser. I konstruktøren vår trenger vi bare å sette OpenGL-versjonen med public FractalSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mRenderer = new FractalRenderer(); setRenderer(mRenderer); }
og også lage og sette rendereren vår:
setRenderMode(int renderMode)
I tillegg kan vi angi attributter som gjengivelsesmodus med RENDERMODE_WHEN_DIRTY
. Fordi det å produsere et Mandelbrot-sett kan være veldig kostbart, bruker vi requestRender()
, som bare gjengir scenen ved initialisering og når det ringes eksplisitte anrop til GLSurfaceView
. Flere alternativer for innstillinger finner du i onTouchEvent(MotionEvent event)
BRANN .
Etter at vi har konstruktøren, vil vi sannsynligvis overstyre minst én annen metode: {a, 0, 0, 0, 0, b, 0, 0, 0, 0, c, 0, v_a, v_b, v_c, 1}
, som kan brukes til generell berøringsbasert brukerinngang. Jeg kommer ikke til å gå for mye i detaljer her, da det ikke er hovedfokuset for leksjonen.
Til slutt kommer vi ned til Renderer, som vil være der det meste av arbeidet for belysning eller kanskje endringer i scenen skjer. Først må vi se litt på hvordan matriser fungerer og fungerer i grafikkverdenen.
OpenGL er avhengig av bruk av matriser. Matriser er en fantastisk kompakt måte å representere sekvenser av generalisert endringer i koordinater . Normalt tillater de oss å gjøre vilkårlige rotasjoner, utvidelser / sammentrekninger og refleksjoner, men med litt finesse kan vi også gjøre oversettelser. I det vesentlige betyr alt at du enkelt kan utføre noe rimelig endre deg, inkludert å flytte et kamera eller få et objekt til å vokse. Av multiplisere våre matriser med en vektor representerer koordinaten vår, kan vi effektivt produsere det nye koordinatsystemet.
De Matrise klasse gitt av OpenGL gir en rekke ferdige måter å beregne matriser vi trenger, men å forstå hvordan de fungerer er en smart idé selv når du arbeider med enkle transformasjoner.
Først kan vi gå over hvorfor vi vil bruke firedimensjonale vektorer og matriser til å håndtere koordinater. Dette går faktisk tilbake til ideen om å finessere vår bruk av koordinater for å kunne gjøre oversettelser: mens en oversettelse i 3D-rom er umulig å bruke bare tre dimensjoner, legger til en fjerde dimensjon muligheten.
For å illustrere dette kan vi bruke en veldig grunnleggende generell skala / oversettelsesmatrise:
Som et viktig notat er OpenGL-matriser kolonnemessige, så denne matrisen vil bli skrevet som onSurfaceCreated(GL10 gl, EGLConfig config)
, som er vinkelrett på hvordan den vanligvis blir lest. Dette kan rasjonaliseres ved å sikre at vektorer, som vises i multiplikasjon som en kolonne, har samme format som matriser.
Bevæpnet med denne kunnskapen om matriser, kan vi gå tilbake til å designe vår renderer. Vanligvis lager vi en matrise i denne klassen som er dannet av produktet av tre matriser: Model, View og Projection. Dette vil på riktig måte bli kalt en MVPMatrix. Du kan lære mer om detaljene her , ettersom vi skal bruke et mer grunnleggende sett med transformasjoner — Mandelbrot-settet er en todimensjonal fullskjermmodell, og det krever egentlig ikke ideen om et kamera.
La oss først sette opp klassen. Vi må implementere nødvendige metoder for Renderer-grensesnittet: onSurfaceChanged(GL10 gl, int width, int height)
, onDrawFrame(GL10 gl)
, og public class FractalRenderer implements GLSurfaceView.Renderer { //Provide a tag for logging errors private static final String TAG = 'FractalRenderer'; //Create all models private Fractal mFractal; //Transformation matrices private final float[] mMVPMatrix = new float[16]; //Any other private variables needed for transformations @Override public void onSurfaceCreated(GL10 unused, EGLConfig config) { // Set the background frame color GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); //Instantiate all models mFractal = new Fractal(); } @Override public void onDrawFrame(GL10 unused) { //Clear the frame of any color information or depth information GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); //Create a basic scale/translate matrix float[] mMVPMatrix = new float[]{ -1.0f/mZoom, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f/(mZoom*mRatio), 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, -mX, -mY, 0.0f, 1.0f}; //Pass the draw command down the line to all models, giving access to the transformation matrix mFractal.draw(mMVPMatrix); } @Override public void onSurfaceChanged(GL10 unused, int width, int height) { //Create the viewport as being fullscreen GLES20.glViewport(0, 0, width, height); //Change any projection matrices to reflect changes in screen orientation } //Other public access methods for transformations }
. Hele klassen vil ende opp med å se ut som dette:
checkGLError
Det er også to verktøy som brukes i den angitte koden, loadShaders
og public Fractal() { // initialize vertex byte buffer for shape coordinates ByteBuffer bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float) squareCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(squareCoords); vertexBuffer.position(0); // initialize byte buffer for the draw list ByteBuffer dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short) drawOrder.length * 2); dlb.order(ByteOrder.nativeOrder()); drawListBuffer = dlb.asShortBuffer(); drawListBuffer.put(drawOrder); drawListBuffer.position(0); // Prepare shaders int vertexShader = FractalRenderer.loadShader( GLES20.GL_VERTEX_SHADER, vertexShaderCode); int fragmentShader = FractalRenderer.loadShader( GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode); // create empty OpenGL Program mProgram = GLES20.glCreateProgram(); // add the vertex shader to program GLES20.glAttachShader(mProgram, vertexShader); // add the fragment shader to program GLES20.glAttachShader(mProgram, fragmentShader); // create OpenGL program executables GLES20.glLinkProgram(mProgram); }
for å hjelpe til med feilsøking og bruk av skyggeleggere.
I alt dette fortsetter vi å føre kommandokjeden nedover linjen for å kapsle inn de forskjellige delene av programmet. Vi har endelig kommet til det punktet hvor vi kan skrive hva programmet vårt egentlig er gjør , i stedet for hvordan vi kan gjøre teoretiske endringer i det. Når du gjør dette, må vi lage en modellklasse som inneholder informasjonen som må vises for et gitt objekt i scenen. I komplekse 3D-scener kan dette være et dyr eller en tekanne, men vi skal gjøre en fraktal som et langt enklere 2D-eksempel.
I modellklasser skriver vi hele klassen - det er ingen superklasser som må brukes. Vi trenger bare å ha en konstruktør og en slags tegnemetode som tar noen parametere inn.
Når det er sagt, er det fortsatt en rekke variabler vi trenger å ha som egentlig er kjeleplate. La oss ta en titt på den nøyaktige konstruktøren som brukes i Fractal-klassen:
static float squareCoords[] = { -1.0f, 1.0f, 0.0f, // top left -1.0f, -1.0f, 0.0f, // bottom left 1.0f, -1.0f, 0.0f, // bottom right 1.0f, 1.0f, 0.0f }; // top right private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
Ganske en munnfull, ikke sant? Heldigvis er dette en del av programmet som du ikke trenger å endre i det hele tatt, lagre navnet på modellen. Forutsatt at du endrer klassevariablene riktig, bør dette fungere bra for grunnleggende former.
For å diskutere deler av dette, la oss se på noen variable erklæringer:
squareCoords
I (-1,-1)
spesifiserer vi alle koordinatene til firkanten. Merk at alle koordinatene på skjermen er representert som et rutenett med (1,1)
nederst til venstre og drawOrder
øverst til høyre.
I 0
spesifiserer vi rekkefølgen på koordinatene basert på trekanter som utgjør firkanten. Spesielt for konsistens og hastighet bruker OpenGL trekanter for å representere alle overflater. For å lage en firkant, klipper du bare en diagonal (i dette tilfellet 2
til ByteBuffers
) for å gi to trekanter.
For å legge til begge disse i programmet, må du først konvertere dem til en rå bytebuffer for direkte å koble innholdet i matrisen med OpenGL-grensesnittet. Java lagrer matriser som objekter som inneholder tilleggsinformasjon som ikke er direkte kompatibel med pekerbaserte C-matriser som OpenGLs implementering bruker. For å avhjelpe dette, loadShader(int type, String shaderCode)
brukes, som lagrer tilgang til det rå minnet i matrisen.
Etter at vi har lagt inn dataene for toppunktene og tegnet rekkefølge, må vi lage skyggelister.
Når du lager en modell, må det lages to skyggelister: en Vertex Shader og en Fragment (Pixel) Shader. Alle skyggeleggere er skrevet i GL Shading Language (GLSL), som er et C-basert språk med tillegg av et antall innebygde funksjoner , variable modifikatorer , primitiver , og standard inngang / utgang . På Android vil disse bli sendt som endelige strenger gjennom const
, en av de to ressursmetodene i Renderer. La oss først gå gjennom de forskjellige typene kvalifiseringer:
uniform
: Enhver endelig variabel kan deklareres som en konstant, slik at verdien kan lagres for enkel tilgang. Tall som π kan deklareres som konstanter hvis de brukes ofte gjennom skyggen. Det er sannsynlig at kompilatoren automatisk vil erklære umodifiserte verdier som konstanter, avhengig av implementeringen.varying
: Ensartede variabler er de som blir erklært konstant for en enkelt gjengivelse. De brukes hovedsakelig som statiske argumenter for skyggeleggerne dine.attribute
: Hvis en variabel blir deklarert som varierende og er satt i en toppunktskygge, blir den interpolert lineært i fragmentskyggen. Dette er nyttig for å lage en hvilken som helst gradient i farger og er implisitt for dybdeforandringer.vec2
: Attributter kan betraktes som ikke-statiske argumenter for en skyggelegging. De angir settet med innganger som er toppunkt-spesifikke og vil bare vises i Vertex Shaders.I tillegg bør vi diskutere to andre typer primitiver som er lagt til:
vec3
, vec4
, mat2
: Flytende punktvektorer med gitt dimensjon.mat3
, mat4
, x
: Flytpunktsmatriser av gitt dimensjon.Vektorer er tilgjengelige med komponentene y
, z
, w
, og r
eller g
, b
, a
, og vec3 a
. De kan også generere hvilken som helst størrelsesvektor med flere indekser: for a.xxyz
, vec4
returnerer a a
med de tilsvarende verdiene på mat2 matrix
.
Matriser og vektorer kan også indekseres som matriser, og matriser vil returnere en vektor med bare en komponent. Dette betyr at for matrix[0].a
, matrix[0][0]
er gyldig og returnerer vec2 a = vec2(1.0,1.0); vec2 b = a; b.x=2.0;
. Når du arbeider med disse, må du huske at de fungerer som primitive, ikke objekter. Tenk for eksempel på følgende kode:
a=vec2(1.0,1.0)
Dette etterlater b=vec2(2.0,1.0)
og b
, som ikke er hva man kan forvente av objektatferd, hvor den andre linjen vil gi a
en peker til private final String vertexShaderCode = 'attribute vec4 vPosition;' + 'void main() {' + ' gl_Position = vPosition;' + '}';
.
I Mandelbrot-settet vil flertallet av koden være i fragmentskyggen, som er skyggen som kjører på hver piksel. Nominelt fungerer vertex shaders på hvert toppunkt, inkludert attributter som vil være per toppunktbasis, som endringer i farge eller dybde. La oss ta en titt på den ekstremt enkle toppunktskyggen for en fraktal:
gl_Position
I dette, gl_Position
er en utgangsvariabel definert av OpenGL for å registrere koordinatene til et toppunkt. I dette tilfellet passerer vi i en posisjon for hvert toppunkt som vi setter vPosition
. I de fleste applikasjoner vil vi multiplisere MVPMatrix
av en fragmentShaderCode
, som transformerer hjørnene våre, men vi vil at fraktalen alltid skal være fullskjerm. Alle transformasjoner vil bli gjort med et lokalt koordinatsystem.
Fragment Shader kommer til å være der det meste av arbeidet gjøres for å generere settet. Vi setter precision highp float; uniform mat4 uMVPMatrix; void main() { //Scale point by input transformation matrix vec2 p = (uMVPMatrix * vec4(gl_PointCoord,0,1)).xy; vec2 c = p; //Set default color to HSV value for black vec3 color=vec3(0.0,0.0,0.0); //Max number of iterations will arbitrarily be defined as 100. Finer detail with more computation will be found for larger values. for(int i=0;i4.0){ //The point, c, is not part of the set, so smoothly color it. colorRegulator increases linearly by 1 for every extra step it takes to break free. float colorRegulator = float(i-1)-log(((log(dot(p,p)))/log(2.0)))/log(2.0); //This is a coloring algorithm I found to be appealing. Written in HSV, many functions will work. color = vec3(0.95 + .012*colorRegulator , 1.0, .2+.4*(1.0+sin(.3*colorRegulator))); break; } } //Change color from HSV to RGB. Algorithm from https://gist.github.com/patriciogonzalezvivo/114c1653de9e3da6e1e3 vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); vec3 m = abs(fract(color.xxx + K.xyz) * 6.0 - K.www); gl_FragColor.rgb = color.z * mix(K.xxx, clamp(m - K.xxx, 0.0, 1.0), color.y); gl_FragColor.a=1.0; }
til følgende:
fract
Mye av koden er bare matte og algoritme for hvordan settet fungerer. Legg merke til bruken av flere innebygde funksjoner: abs
, mix
, sin
, clamp
og dot
, som alle fungerer på vektorer eller skalarer og returnerer vektorer eller skalarer. I tillegg draw
brukes som tar vektorargumenter og returnerer en skalar.
Nå som vi har satt skyggeleggingene våre opp til bruk, har vi et siste trinn, som er å implementere public void draw(float[] mvpMatrix) { // Add program to OpenGL environment GLES20.glUseProgram(mProgram); // get handle to vertex shader's vPosition member mPositionHandle = GLES20.glGetAttribLocation(mProgram, 'vPosition'); mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, 'uMVPMatrix'); //Pass uniform transformation matrix to shader GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); //Add attribute array of vertices GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer( mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer); // Draw the square GLES20.glDrawElements( GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer); // Disable vertex array GLES20.glDisableVertexAttribArray(mPositionHandle); FractalRenderer.checkGlError('Test'); }
funksjon i vår modell:
uniform
Funksjonen sender alle argumenter til skyggeleggerne, inkludert attribute
transformasjonsmatrise og double
stilling.
Etter å ha samlet alle delene av programmet, kan vi endelig gi det et løp. Forutsatt at riktig berøringsstøtte håndteres, vil absolutt fascinerende scener bli malt:
Hvis vi zoomer inn litt mer, begynner vi å merke et brudd i bildet:
Dette har absolutt ingenting å gjøre med matematikken til settet bak og alt å gjøre med hvordan tall blir lagret og behandlet i OpenGL. Mens nyere støtte for float
presisjon er laget, støtter OpenGL 2.0 ikke mer enn precision highp float
s. Vi spesifiserte dem spesifikt til å være den høyeste presisjonsflyten som er tilgjengelig med double
i skyggen vår, men selv det er ikke bra nok.
For å komme rundt dette problemet, vil den eneste måten være å etterligne float
s bruker to GLSurfaceView
s. Denne metoden kommer faktisk i størrelsesorden av den faktiske presisjonen til en innfødt implementert, selv om det er ganske alvorlige kostnader å øke hastigheten. Dette vil bli igjen som en øvelse for leseren, hvis man ønsker å ha et høyere nøyaktighetsnivå.
Med noen få støtteklasser kan OpenGL raskt opprettholde gjengivelse i sanntid av komplekse scener. Å lage et layout sammensatt av et Renderer
, sette dets