So verwenden Sie Android-Medieneffekte mit OpenGL ES

Mit dem Media Effects-Framework von Android können Entwickler auf beeindruckende Weise visuelle Effekte auf Fotos und Videos anwenden. Da das Framework die GPU für alle Bildverarbeitungsvorgänge verwendet, kann es nur OpenGL-Texturen als Eingabe akzeptieren. In diesem Lernprogramm erfahren Sie, wie Sie mithilfe von OpenGL ES 2.0 eine Ressource mit Zeichenstruktur in eine Textur konvertieren und anschließend das Framework verwenden, um verschiedene Effekte darauf anzuwenden.

Voraussetzungen

Um diesem Tutorial zu folgen, benötigen Sie:

  • eine IDE, die Android-Anwendungsentwicklung unterstützt. Wenn Sie noch keine haben, holen Sie sich die neueste Version von Android Studio von der Android Developer-Website.
  • ein Gerät, auf dem Android 4.0 oder höher ausgeführt wird und über eine GPU verfügt, die OpenGL ES 2.0 unterstützt.
  • ein grundlegendes Verständnis von OpenGL.

1. Einrichten der OpenGL ES-Umgebung

Schritt 1: Erstellen Sie eine GLSurfaceView

Um OpenGL-Grafiken in Ihrer App anzuzeigen, müssen Sie ein verwenden GLSurfaceView Objekt. Wie jeder andere Aussicht, Sie können es zu einem hinzufügen Aktivität oder Fragment Definieren Sie es in einer Layout-XML-Datei oder erstellen Sie eine Instanz davon im Code.

In diesem Tutorial haben Sie eine GLSurfaceView Objekt als einziges Aussicht in deiner Aktivität. Daher ist die Erstellung im Code einfacher. Nach dem Erstellen übergeben Sie es an die setContentView Methode, so dass es den gesamten Bildschirm ausfüllt. Ihre Aktivität's onCreate Methode sollte so aussehen:

protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); GLSurfaceView view = new GLSurfaceView (this); setContentView (Ansicht); 

Da das Media Effects-Framework nur OpenGL ES 2.0 oder höher unterstützt, übergeben Sie den Wert 2 zum setEGLContextClientVersion Methode.

view.setEGLContextClientVersion (2);

Um sicherzustellen, dass die GLSurfaceView macht seinen Inhalt nur bei Bedarf wieder, übergeben Sie den Wert RENDERMODE_WHEN_DIRTY zum setRenderMode Methode.

view.setRenderMode (GLSurfaceView.RENDERMODE_WHEN_DIRTY);

Schritt 2: Erstellen Sie einen Renderer

EIN GLSurfaceView.Renderer ist verantwortlich für das Zeichnen des Inhalts der GLSurfaceView.

Erstellen Sie eine neue Klasse, die das implementiert GLSurfaceView.Renderer Schnittstelle. Ich werde diese Klasse anrufen EffectsRenderer. Nachdem Sie einen Konstruktor hinzugefügt und alle Methoden der Schnittstelle überschrieben haben, sollte die Klasse folgendermaßen aussehen:

public class EffectsRenderer implementiert GLSurfaceView.Renderer public EffectsRenderer (Kontext-Kontext) super ();  @Override public void onSurfaceCreated (GL10 gl, EGLConfig config)  @Override public void onSurfaceChanged (GL10 gl, Breite, int Höhe)  @Override public void onDrawFrame (GL10 gl) 

Geh zurück zu deinem Aktivität und rufen Sie an setRenderer Methode, so dass die GLSurfaceView verwendet den benutzerdefinierten Renderer.

view.setRenderer (neuer EffectsRenderer (this));

Schritt 3: Bearbeiten Sie das Manifest

Wenn Sie Ihre App in Google Play veröffentlichen möchten, fügen Sie Folgendes zu hinzu AndroidManifest.xml:

Dadurch wird sichergestellt, dass Ihre App nur auf Geräten installiert werden kann, die OpenGL ES 2.0 unterstützen. Die OpenGL-Umgebung ist jetzt bereit.

2. Erstellen einer OpenGL-Ebene

Schritt 1: Scheitelpunkte definieren

Das GLSurfaceView kann ein Foto nicht direkt anzeigen. Das Foto muss zuerst in eine Textur umgewandelt und auf eine OpenGL-Form angewendet werden. In diesem Lernprogramm erstellen wir eine 2D-Ebene mit vier Scheitelpunkten. Lassen Sie uns der Einfachheit halber ein Quadrat bilden. Erstellen Sie eine neue Klasse, Quadrat, das Quadrat darstellen.

öffentliche Klasse Square 

Das Standard-OpenGL-Koordinatensystem hat seinen Ursprung in der Mitte. Als Ergebnis sind die Koordinaten der vier Ecken unseres Quadrats, deren Seiten sind zwei Einheiten lang wird sein:

  • linke untere Ecke bei (-1, -1)
  • rechte untere Ecke bei (1, -1)
  • rechte obere Ecke bei (1, 1)
  • obere linke Ecke um (-1, 1)

Alle Objekte, die wir mit OpenGL zeichnen, sollten aus Dreiecken bestehen. Um das Quadrat zu zeichnen, benötigen wir zwei Dreiecke mit einer gemeinsamen Kante. Dies bedeutet, dass die Koordinaten der Dreiecke sind:

Dreieck 1: (-1, -1), (1, -1) und (-1, 1)
Dreieck 2: (1, -1), (-1, 1) und (1, 1)

Ein ... kreieren schweben Array, um diese Scheitelpunkte darzustellen.

Private Float-Knoten [] = -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f,;

Um die Textur auf das Quadrat abzubilden, müssen Sie die Koordinaten der Scheitelpunkte der Textur angeben. Texturen folgen einem Koordinatensystem, in dem der Wert der y-Koordinate steigt, je höher Sie stehen. Erstellen Sie ein anderes Array, um die Scheitelpunkte der Textur darzustellen.

private float textureVertices [] = 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f;

Schritt 2: Erstellen Sie Pufferobjekte

Die Koordinaten-Arrays müssen in Byte-Puffer umgewandelt werden, bevor OpenGL sie verwenden kann. Lassen Sie uns zuerst diese Puffer deklarieren.

private FloatBuffer verticesBuffer; privater FloatBuffer textureBuffer;

Schreiben Sie den Code, um diese Puffer in einer neuen aufgerufenen Methode zu initialisieren initializeBuffers. Verwenden Sie die ByteBuffer.allocateDirect Methode zum Erstellen des Puffers. Weil ein schweben Verwendet 4 Bytes, müssen Sie die Größe der Arrays mit dem Wert multiplizieren 4.

Als nächstes verwenden ByteBuffer.nativeOrder um die Byte-Reihenfolge der zugrunde liegenden systemeigenen Plattform zu bestimmen und die Reihenfolge der Puffer auf diesen Wert festzulegen. Verwenden Sie die asFloatBuffer Methode zur Konvertierung der ByteBuffer Beispiel in eine FloatBuffer. Nach dem FloatBuffer erstellt wird, verwenden Sie die stellen Methode, um das Array in den Puffer zu laden. Verwenden Sie schließlich die Position Methode, um sicherzustellen, dass der Puffer von Anfang an gelesen wird.

Der Inhalt der initializeBuffers Methode sollte so aussehen:

private void initializeBuffers () ByteBuffer buff = ByteBuffer.allocateDirect (vertices.length * 4); buff.order (ByteOrder.nativeOrder ()); verticesBuffer = buff.asFloatBuffer (); verticesBuffer.put (vertices); verticesBuffer.position (0); buff = ByteBuffer.allocateDirect (textureVertices.length * 4); buff.order (ByteOrder.nativeOrder ()); textureBuffer = buff.asFloatBuffer (); textureBuffer.put (textureVertices); textureBuffer.position (0); 

Schritt 3: Erstellen Sie Shader

Es ist Zeit, eigene Shader zu schreiben. Shader sind nichts weiter als einfache C-Programme, die von der GPU ausgeführt werden, um jeden einzelnen Scheitelpunkt zu verarbeiten. Für dieses Lernprogramm müssen Sie zwei Shader, einen Vertex-Shader und einen Fragment-Shader erstellen.

Der C-Code für den Vertex-Shader lautet:

Attribut vec4 aPosition; Attribut vec2 aTexPosition; variierendes vec2 vTexPosition; void main () gl_Position = aPosition; vTexPosition = aTexPosition; ;

Der C-Code für den Fragment-Shader lautet:

Präzisions-Mediumschwimmer; einheitliche sampler2D uTexture; variierendes vec2 vTexPosition; void main () gl_FragColor = texture2D (uTexture, vTexPosition); ;

Wenn Sie OpenGL bereits kennen, sollte Ihnen dieser Code bekannt sein, da er auf allen Plattformen gleich ist. Wenn Sie diese Programme nicht verstehen, müssen Sie sich auf die OpenGL-Dokumentation beziehen. Hier ist eine kurze Erklärung, um den Einstieg zu erleichtern:

  • Der Vertex-Shader ist für das Zeichnen der einzelnen Vertices verantwortlich. eine Position ist eine Variable, die an das gebunden wird FloatBuffer das enthält die Koordinaten der Scheitelpunkte. Ähnlich, aTexPosition ist eine Variable, die an das gebunden wird FloatBuffer das enthält die Koordinaten der Textur. gl_Position ist eine integrierte OpenGL-Variable und repräsentiert die Position jedes Scheitelpunkts. Das vTexPosition ist ein variierend Variable, deren Wert einfach an den Fragment-Shader übergeben wird.
  • In diesem Lernprogramm ist der Fragment-Shader für die Färbung des Quadrats verantwortlich. Mit der Taste werden Farben aus der Textur aufgenommen texture2D Methode und ordnet sie dem Fragment mithilfe einer eingebauten Variablen namens zu gl_FragColor.

Der Shader-Code muss als dargestellt werden String Objekte in der Klasse.

private final String vertexShaderCode = "Attribut vec4 aPosition;" + "Attribut vec2 aTexPosition;" + "variierendes vec2 vTexPosition;" + "void main () " + "gl_Position = aPosition;" + "vTexPosition = aTexPosition;" + ""; private final String fragmentShaderCode = "precision mediump float;" + "einheitliche sampler2D uTexture;" + "variierendes vec2 vTexPosition;" + "void main () " + "gl_FragColor = texture2D (uTexture, vTexPosition);" + "";

Schritt 4: Erstellen Sie ein Programm

Erstellen Sie eine neue Methode namens initializeProgram Erstellen eines OpenGL-Programms nach dem Kompilieren und Verknüpfen der Shader.

Benutzen glCreateShader ein Shader-Objekt erstellen und einen Verweis darauf in Form eines zurückgeben int. Übergeben Sie den Wert, um einen Vertex-Shader zu erstellen GL_VERTEX_SHADER dazu Um einen Fragment-Shader zu erstellen, übergeben Sie den Wert entsprechend GL_FRAGMENT_SHADER dazu Nächste Verwendung glShaderSource um den entsprechenden Shader-Code mit dem Shader zu verknüpfen. Benutzen glCompileShader um den Shader-Code zu kompilieren.

Nachdem Sie beide Shader kompiliert haben, erstellen Sie ein neues Programm mit glCreateProgram. So wie  glCreateShader, auch das gibt ein zurück int als Referenz zum Programm. Anruf glAttachShader um die Shader an das Programm anzuhängen. Schließlich rufen Sie an glLinkProgram um das Programm zu verlinken.

Ihre Methode und die zugehörigen Variablen sollten folgendermaßen aussehen:

private int vertexShader; private int fragmentShader; privates int-Programm; private void initializeProgram () vertexShader = GLES20.glCreateShader (GLES20.GL_VERTEX_SHADER); GLES20.glShaderSource (vertexShader, vertexShaderCode); GLES20.glCompileShader (vertexShader); fragmentShader = GLES20.glCreateShader (GLES20.GL_FRAGMENT_SHADER); GLES20.glShaderSource (fragmentShader, fragmentShaderCode); GLES20.glCompileShader (fragmentShader); program = GLES20.glCreateProgram (); GLES20.glAttachShader (Programm, vertexShader); GLES20.glAttachShader (Programm, fragmentShader); GLES20.glLinkProgram (Programm);  

Möglicherweise haben Sie festgestellt, dass die OpenGL-Methoden (die Methoden mit dem Präfix) beginnen gl) gehören zur Klasse GLES20. Dies liegt daran, dass wir OpenGL ES 2.0 verwenden. Wenn Sie eine höhere Version verwenden möchten, müssen Sie die Klassen verwenden GLES30 oder GLES31.

Schritt 5: Zeichnen Sie das Quadrat

Erstellen Sie eine neue Methode namens zeichnen Um das Quadrat tatsächlich mit den zuvor definierten Scheitelpunkten und Shadern zu zeichnen.

Folgendes müssen Sie bei dieser Methode tun:

  1. Benutzen glBindFramebuffer Erstellen eines benannten Frame-Pufferobjekts (häufig als FBO bezeichnet).
  2. Benutzen glUseProgram um das Programm zu verwenden, das wir gerade verlinkt haben.
  3. Übergeben Sie den Wert GL_BLEND zu glDisable um das Mischen von Farben während des Renderns zu deaktivieren.
  4. Benutzen glGetAttribLocation um die Variablen in den Griff zu bekommen eine Position und aTexPosition im Vertex-Shader-Code erwähnt.
  5. Benutzen glGetUniformLocation die Konstante in den Griff bekommen uTextur im Fragment-Shader-Code erwähnt.
  6. Verwenden Sie die glVertexAttribPointer das verbinden eine Position und aTexPosition behandelt mit dem verticesBuffer und das textureBuffer beziehungsweise.
  7. Benutzen glBindTexture um die Textur zu binden (wird als Argument an die übergeben zeichnen method) an den Fragment-Shader.
  8. Löschen Sie den Inhalt der GLSurfaceView mit glClear.
  9. Verwenden Sie schließlich die glDrawArrays Methode, um tatsächlich die beiden Dreiecke (und damit das Quadrat) zu zeichnen.

Der Code für die zeichnen Methode sollte so aussehen:

public void draw (int texture) GLES20.glBindFramebuffer (GLES20.GL_FRAMEBUFFER, 0); GLES20.glUseProgram (Programm); GLES20.glDisable (GLES20.GL_BLEND); int positionHandle = GLES20.glGetAttribLocation (Programm, "aPosition"); int textureHandle = GLES20.glGetUniformLocation (Programm, "uTexture"); int texturePositionHandle = GLES20.glGetAttribLocation (Programm, "aTexPosition"); GLES20.glVertexAttribPointer (texturePositionHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer); GLES20.glEnableVertexAttribArray (texturePositionHandle); GLES20.glActiveTexture (GLES20.GL_TEXTURE0); GLES20.glBindTexture (GLES20.GL_TEXTURE_2D, Textur); GLES20.glUniform1i (textureHandle, 0); GLES20.glVertexAttribPointer (positionHandle, 2, GLES20.GL_FLOAT, false, 0, verticesBuffer); GLES20.glEnableVertexAttribArray (positionHandle); GLES20.glClear (GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays (GLES20.GL_TRIANGLE_STRIP, 0, 4);  

Fügen Sie der Klasse einen Konstruktor hinzu, um die Puffer und das Programm zum Zeitpunkt der Objekterstellung zu initialisieren.

public Square () initializeBuffers (); initializeProgram (); 

3. Rendern der OpenGL-Ebene und der Textur

Derzeit macht unser Renderer nichts. Wir müssen das ändern, damit es die Ebene rendern kann, die wir in den vorherigen Schritten erstellt haben.

Aber lassen Sie uns zuerst eine Bitmap. Fügen Sie Ihrem Projekt ein beliebiges Foto hinzu res / drawable Mappe. Die Datei, die ich verwende, wird aufgerufen wald.jpg. Verwenden Sie die BitmapFactory um das Foto in eine zu konvertieren Bitmap Objekt. Speichern Sie auch die Abmessungen der Bitmap Objekt in separaten Variablen.

Ändern Sie den Konstruktor von EffectsRenderer Klasse, so dass es den folgenden Inhalt hat:

privates Bitmap-Foto; private int photoWidth, photoHeight; public EffectsRenderer (Kontext-Kontext) super (); photo = BitmapFactory.decodeResource (context.getResources (), R.drawable.forest); photoWidth = photo.getWidth (); photoHeight = photo.getHeight (); 

Erstellen Sie eine neue Methode namens generateSquare um die Bitmap in eine Textur zu konvertieren und zu initialisieren a Quadrat Objekt. Sie benötigen außerdem ein Array von Ganzzahlen, um Verweise auf die OpenGL-Texturen aufzunehmen. Benutzen glGenTextures das Array zu initialisieren und glBindTexture um die Textur beim Index zu aktivieren 0.

Als nächstes verwenden glTexParameteri um verschiedene Eigenschaften festzulegen, die entscheiden, wie die Textur gerendert wird:

  • einstellen GL_TEXTURE_MIN_FILTER (die Minifizierungsfunktion) und die GL_TEXTURE_MAG_FILTER (die Vergrößerungsfunktion) auf GL_LINEAR um sicherzustellen, dass die textur glatt aussieht, auch wenn sie gedehnt oder geschrumpft ist.
  • einstellen GL_TEXTURE_WRAP_S und GL_TEXTURE_WRAP_T zu GL_CLAMP_TO_EDGE so dass die Textur nie wiederholt wird.

Verwenden Sie schließlich die texImage2D Methode zur Zuordnung der Bitmap auf die Textur Die Umsetzung der generateSquare Methode sollte so aussehen:

private int textures [] = neues int [2]; privater Platz; private void generateSquare () GLES20.glGenTextures (2, textures, 0); GLES20.glBindTexture (GLES20.GL_TEXTURE_2D, Texturen [0]); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri (GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLUtils.texImage2D (GLES20.GL_TEXTURE_2D, 0, Foto, 0); Quadrat = neues Quadrat (); 

Wann immer die Abmessungen des GLSurfaceView ändere das onSurfaceChanged Methode der Renderer wird genannt. Hier müssen Sie anrufen glViewPort um die neuen Dimensionen des Ansichtsfensters anzugeben. Rufen Sie auch an glClearColor malen die GLSurfaceView schwarz. Rufen Sie an generateSquare um die Texturen und die Ebene neu zu initialisieren.

@Override public void onSurfaceChanged (GL10 gl, int width, int height) GLES20.glViewport (0,0, width, height); GLES20.glClearColor (0,0,0,1); generateSquare (); 

Zum Schluss rufen Sie die Quadrat Objekt ist zeichnen Methode innerhalb der onDrawFrame Methode der Renderer.

@Override public void onDrawFrame (GL10 gl) square.draw (Texturen [0]); 

Sie können jetzt Ihre App ausführen und das von Ihnen ausgewählte Foto als OpenGL-Textur auf einer Ebene anzeigen lassen.

4. Verwenden des Media Effects Framework

Der komplexe Code, den wir bisher geschrieben haben, war nur eine Voraussetzung für die Verwendung des Media Effects-Frameworks. Es ist jetzt an der Zeit, das Framework selbst zu verwenden. Fügen Sie folgende Felder hinzu Renderer Klasse.

private EffectContext effectContext; privater Effekteffekt;

Initialisieren Sie die effectContext Feld mit der EffectContext.createWithCurrentGlContext. Es ist für die Verwaltung der Informationen zu den visuellen Effekten in einem OpenGL-Kontext verantwortlich. Um die Leistung zu optimieren, sollte dies nur einmal aufgerufen werden. Fügen Sie den folgenden Code am Anfang Ihres ein onDrawFrame Methode.

if (effectContext == null) effectContext = EffectContext.createWithCurrentGlContext (); 

Einen Effekt zu erzeugen ist sehr einfach. Verwenden Sie die effectContext ein erstellen EffectFactory und benutze die EffectFactory ein erstellen Bewirken Objekt. Einmal ein Bewirken Objekt ist verfügbar, Sie können anrufen sich bewerben und geben Sie einen Verweis auf die ursprüngliche Textur an, in unserem Fall ist es das Texturen [0], zusammen mit einem Verweis auf ein leeres Texturobjekt, in unserem Fall ist es das Texturen [1]. Nach dem sich bewerben Methode wird aufgerufen, Texturen [1] enthält das Ergebnis der Bewirken.

Zum Beispiel, um das zu erstellen und anzuwenden Graustufen Effekt, hier ist der Code, den Sie schreiben müssen:

private void grayScaleEffect () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_GRAYSCALE); effect.apply (Texturen [0], photoWidth, photoHeight, Texturen [1]); 

Rufen Sie diese Methode in auf onDrawFrame und weitergeben Texturen [1] zum Quadrat Objekt ist zeichnen Methode. Ihre onDrawFrame Methode sollte den folgenden Code haben:

@Override public void onDrawFrame (GL10 gl) if (effectContext == null) effectContext = EffectContext.createWithCurrentGlContext ();  if (effect! = null) effect.release ();  grayScaleEffect (); square.draw (Texturen [1]); 

Das Veröffentlichung Diese Methode wird verwendet, um alle Ressourcen eines Systems freizugeben Bewirken. Wenn Sie die App ausführen, sollten Sie das folgende Ergebnis sehen:

Sie können den gleichen Code verwenden, um andere Effekte anzuwenden. Hier ist zum Beispiel der Code zum Anwenden von Dokumentarfilm bewirken:

private void documentaryEffect () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_DOCUMENTARY); effect.apply (Texturen [0], photoWidth, photoHeight, Texturen [1]); 

Das Ergebnis sieht so aus:

Einige Effekte benötigen Parameter. Zum Beispiel hat der Helligkeitseinstellungseffekt eine Helligkeit Parameter, der a braucht schweben Wert. Sie können verwenden setParameter um den Wert eines Parameters zu ändern. Der folgende Code zeigt Ihnen, wie Sie es verwenden:

private void HelligkeitEffekt () EffectFactory factory = effectContext.getFactory (); effect = factory.createEffect (EffectFactory.EFFECT_BRIGHTNESS); effect.setParameter ("Helligkeit", 2f); effect.apply (Texturen [0], photoWidth, photoHeight, Texturen [1]); 

Der Effekt bewirkt, dass Ihre App das folgende Ergebnis rendert:

Fazit

In diesem Lernprogramm haben Sie gelernt, wie Sie mit Media Effects Framework verschiedene Effekte auf Ihre Fotos anwenden. Sie haben dabei auch gelernt, mit OpenGL ES 2.0 eine Ebene zu zeichnen und verschiedene Texturen darauf anzuwenden.

Das Framework kann sowohl auf Fotos als auch auf Videos angewendet werden. Bei Videos müssen Sie den Effekt einfach auf die einzelnen Frames des Videos in der Grafik anwenden onDrawFrame Methode.

Sie haben bereits drei Effekte in diesem Lernprogramm gesehen, und das Framework hat Dutzende mehr, mit denen Sie experimentieren können. Weitere Informationen finden Sie auf der Android Developer-Website.