web-dev-qa-db-fra.com

OpenGL dessiner sur Android en combinant avec Unity pour transférer la texture à travers le tampon d'images ne peut pas fonctionner

Je crée actuellement un plug-in de lecteur Android pour Unity. L'idée de base est que je lirai la vidéo par MediaPlayer sur Android, qui fournit une API setSurface recevant un SurfaceTexture en tant que paramètre constructeur et se liant à la fin à une texture OpenGL-ES. Dans la plupart des autres cas, comme l'affichage d'une image, nous pouvons simplement envoyer cette texture sous forme de pointeur/id à Unity, appeler Texture2D.CreateExternalTexture pour générer un objet Texture2D et le définir sur une interface utilisateur GameObject pour restituer l'image. Cependant, lorsqu'il s'agit d'afficher des images vidéo, c'est un peu différent depuis la lecture d'une vidéo sur Android nécessite une texture de type GL_TEXTURE_EXTERNAL_OES alors que Unity ne prend en charge que le type universel GL_TEXTURE_2D.

Pour résoudre le problème, j'ai cherché sur Google pendant un moment et je savais que je devais adopter une sorte de technologie appelée "Render to texture". Plus clairement, je devrais générer 2 textures, une pour les MediaPlayer et SurfaceTexture dans Android afin de recevoir des images vidéo et une autre pour Unity qui devrait également contenir les données d’image. Le premier doit être de type GL_TEXTURE_EXTERNAL_OES (appelons-le texture OES en abrégé) et le second de type GL_TEXTURE_2D (appelons-le texture 2D). Ces deux textures générées sont vides au début. Lorsque lié avec MediaPlayer, la texture OES sera mise à jour pendant la lecture de la vidéo, puis nous pouvons utiliser une FrameBuffer pour dessiner le contenu de la texture OES sur la texture 2D.

J'ai écrit une version purement Android de ce processus et cela fonctionne plutôt bien lorsque je dessine enfin la texture 2D sur l'écran. Cependant, lorsque je le publie sous la forme d'un plug-in Unity Android et que le même code est exécuté sous Unity, aucune image ne s'affiche. Au lieu de cela, il affiche uniquement une couleur prédéfinie à partir de glClearColor, ce qui signifie deux choses:

  1. Le processus de transfert de la texture OES -> FrameBuffer -> la texture 2D est terminée et Unity reçoit la texture 2D finale. Parce que la glClearColor est appelée uniquement lorsque nous attirons le contenu de la texture OES vers FrameBuffer.
  2. Il y a eu quelques erreurs lors du dessin après glClearColor, car nous ne voyons pas les images de la vidéo. En fait, j'appelle aussi glReadPixels après le dessin et avant la suppression de la liaison avec la FrameBuffer, qui lit les données de la FrameBuffer avec laquelle nous sommes liés. Et elle renvoie la valeur de la couleur unique identique à celle que nous avons définie dans glClearColor.

Afin de simplifier le code que je devrais fournir ici, je vais tracer un triangle sur une texture 2D à travers FrameBuffer. Si nous pouvons déterminer quelle partie est la mauvaise, nous pouvons alors résoudre facilement le problème similaire en traçant des images vidéo.

La fonction sera appelée sur Unity:

  public int displayTriangle() {
    Texture2D texture = new Texture2D(UnityPlayer.currentActivity);
    texture.init();

    Triangle triangle = new Triangle(UnityPlayer.currentActivity);
    triangle.init();

    TextureTransfer textureTransfer = new TextureTransfer();
    textureTransfer.tryToCreateFBO();

    mTextureWidth = 960;
    mTextureHeight = 960;
    textureTransfer.tryToInitTempTexture2D(texture.getTextureID(), mTextureWidth, mTextureHeight);

    textureTransfer.fboStart();
    triangle.draw();
    textureTransfer.fboEnd();

    // Unity needs a native texture id to create its own Texture2D object
    return texture.getTextureID();
  }

Initialisation de la texture 2D:

  protected void initTexture() {
    int[] idContainer = new int[1];
    GLES30.glGenTextures(1, idContainer, 0);
    textureId = idContainer[0];
    Log.i(TAG, "texture2D generated: " + textureId);
    // texture.getTextureID() will return this textureId

    bindTexture();

    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
    GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_Edge);
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_Edge);

    unbindTexture();
  }

  public void bindTexture() {
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);
  }

  public void unbindTexture() {
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
  }

draw() of Triangle:

  public void draw() {
    float[] vertexData = new float[] {
        0.0f,  0.0f, 0.0f,
        1.0f, -1.0f, 0.0f,
        1.0f,  1.0f, 0.0f
    };
    vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer()
        .put(vertexData);
    vertexBuffer.position(0);

    GLES30.glClearColor(0.0f, 0.0f, 0.9f, 1.0f);
    GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT);
    GLES30.glUseProgram(mProgramId);

    vertexBuffer.position(0);
    GLES30.glEnableVertexAttribArray(aPosHandle);
    GLES30.glVertexAttribPointer(
        aPosHandle, 3, GLES30.GL_FLOAT, false, 12, vertexBuffer);

    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 3);
  }

vertex shader of Triangle:

attribute vec4 aPosition;
void main() {
  gl_Position = aPosition;
}

fragment shader of Triangle:

precision mediump float;
void main() {
  gl_FragColor = vec4(0.9, 0.0, 0.0, 1.0);
}

Code clé de TextureTransfer:

  public void tryToInitTempTexture2D(int texture2DId, int textureWidth, int textureHeight) {
    if (mTexture2DId != -1) {
      return;
    }

    mTexture2DId = texture2DId;
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mTexture2DId);
    Log.i(TAG, "glBindTexture " + mTexture2DId + " to init for FBO");

    // make 2D texture empty
    GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, textureWidth, textureHeight, 0,
        GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
    Log.i(TAG, "glTexImage2D, textureWidth: " + textureWidth + ", textureHeight: " + textureHeight);

    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);

    fboStart();
    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0,
        GLES30.GL_TEXTURE_2D, mTexture2DId, 0);
    Log.i(TAG, "glFramebufferTexture2D");
    int fboStatus = GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER);
    Log.i(TAG, "fbo status: " + fboStatus);
    if (fboStatus != GLES30.GL_FRAMEBUFFER_COMPLETE) {
      throw new RuntimeException("framebuffer " + mFBOId + " incomplete!");
    }
    fboEnd();
  }

  public void fboStart() {
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOId);
  }

  public void fboEnd() {
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
  }

Et enfin du code côté Unity:

int textureId = plugin.Call<int>("displayTriangle");
Debug.Log("native textureId: " + textureId);
Texture2D triangleTexture = Texture2D.CreateExternalTexture(
  960, 960, TextureFormat.RGBA32, false, true, (IntPtr) textureId);
triangleTexture.UpdateExternalTexture(triangleTexture.GetNativeTexturePtr());
rawImage.texture = triangleTexture;
rawImage.color = Color.white;

Le code ci-dessus n’affiche pas le triangle attendu, mais seulement un fond bleu. J'ajoute glGetError après presque chaque appel de fonctions OpenGL alors qu'aucune erreur n'est générée.

Ma version d'Unity est 2017.2.1. Pour la construction d'Android, j'ai arrêté le rendu expérimental multithread et les autres paramètres sont tous définis par défaut (pas de compression de la texture, pas de développement, etc.) Le niveau minimal d'API de mon application est 5,0 Lollipop et le niveau cible de l'API est 9,0 Pie.

J'ai vraiment besoin d'aide, merci d'avance!

9
ywwynm

Maintenant, j'ai trouvé la réponse: Si vous souhaitez effectuer des travaux de dessin dans votre plugin, vous devez le faire au niveau de la couche native . Donc, si vous voulez créer un plugin Android, vous devez appeler les API OpenGL-ES à JNI au lieu du côté Java. La raison en est que Unity permet uniquement de dessiner des graphiques sur son fil de rendu. Si vous appelez simplement les API OpenGL-ES comme je l'ai fait du côté de Java dans la description de la question, elles seront exécutées sur le thread principal Unity au lieu du rendu du thread. Unity fournit une méthode, GL.IssuePluginEvent, pour appeler vos propres fonctions sur le fil de rendu, mais elle nécessite un codage natif, cette fonction nécessitant un pointeur de fonction comme rappel. Voici un exemple simple à utiliser:

À JNI côté:

// you can copy these headers from https://github.com/googlevr/gvr-unity-sdk/tree/master/native_libs/video_plugin/src/main/jni/Unity
#include "IUnityInterface.h"
#include "UnityGraphics.h"

static void on_render_event(int event_type) {
  // do all of your jobs related to rendering, including initializing the context,
  // linking shaders, creating program, finding handles, drawing and so on
}

// UnityRenderingEvent is an alias of void(*)(int) defined in UnityGraphics.h
UnityRenderingEvent get_render_event_function() {
  UnityRenderingEvent ptr = on_render_event;
  return ptr;
}

// notice you should return a long value to Java side
extern "C" JNIEXPORT jlong JNICALL
Java_com_abc_xyz_YourPluginClass_getNativeRenderFunctionPointer(JNIEnv *env, jobject instance) {
  UnityRenderingEvent ptr = get_render_event_function();
  return (long) ptr;
}

Du côté Android Java:

class YourPluginClass {
  ...
  public native long getNativeRenderFunctionPointer();
  ...
}

Du côté de l'unité:

private void IssuePluginEvent(int pluginEventType) {
  long nativeRenderFuncPtr = Call_getNativeRenderFunctionPointer(); // call through plugin class
  IntPtr ptr = (IntPtr) nativeRenderFuncPtr;
  GL.IssuePluginEvent(ptr, pluginEventType); // pluginEventType is related to native function parameter event_type
}

void Start() {
  IssuePluginEvent(1); // let's assume 1 stands for initializing everything
  // get your texture2D id from plugin, create Texture2D object from it,
  // attach that to a GameObject, and start playing for the first time
}

void Update() {
  // call SurfaceTexture.updateTexImage in plugin
  IssuePluginEvent(2); // let's assume 2 stands for transferring TEXTURE_EXTERNAL_OES to TEXTURE_2D through FrameBuffer
  // call Texture2D.UpdateExternalTexture to update GameObject's appearance
}

Vous devez toujours transférer la texture et tout ce qui s'y rapporte devrait se produire au niveau de la couche JNI. Mais ne vous inquiétez pas, ils sont presque identiques à ceux décrits dans la description de la question, mais uniquement dans un langage différent de Java et il existe de nombreux documents sur ce processus pour vous permettre de le réaliser.

Enfin, laissez-moi aborder la solution pour résoudre à nouveau ce problème: faites votre travail natif au niveau de la couche native et ne soyez pas accro à Java pur ... Je suis totalement surpris qu'il n'y ait pas de blog/answer/wiki to dites-nous simplement d'écrire notre code en C++. Bien qu'il existe certaines implémentations open-source comme le gvr-unity-sdk de Google, elles fournissent une référence complète, mais vous ne douterez pas que vous pourrez peut-être terminer la tâche sans écrire de code C++. Maintenant, nous savons que nous ne pouvons pas. Cependant, pour être honnête, je pense que Unity a la capacité de rendre ce progrès encore plus facile. 

1
ywwynm