Рисование OpenGL на Android в сочетании с Unity для передачи текстуры через буфер кадров не может работать - PullRequest
0 голосов
/ 30 августа 2018

В настоящее время я делаю плагин для Android-плеера для Unity. Основная идея заключается в том, что я буду воспроизводить видео с помощью MediaPlayer на Android, который предоставляет API setSurface, получающий SurfaceTexture в качестве параметра конструктора и в конце связывающийся с текстурой OpenGL-ES. В большинстве других случаев, таких как показ изображения, мы можем просто отправить эту текстуру в виде указателя / идентификатора в Unity, вызвать Texture2D.CreateExternalTexture там, чтобы сгенерировать объект Texture2D, и установить его в UI GameObject для рендеринга изображения. Однако, когда дело доходит до отображения видеокадров, это немного отличается, так как для воспроизведения видео на Android требуется текстура типа GL_TEXTURE_EXTERNAL_OES, тогда как Unity поддерживает только универсальный тип GL_TEXTURE_2D.

.

Чтобы решить эту проблему, я некоторое время гуглил и знал, что мне следует применить технологию, называемую «Render to texture» . Проще говоря, я должен сгенерировать 2 текстуры: одну для MediaPlayer и SurfaceTexture в Android для получения видеокадров, а другую для Unity, в которой также должны быть данные изображения. Первый должен иметь тип GL_TEXTURE_EXTERNAL_OES (для краткости будем называть его текстурой OES), а второй - тип GL_TEXTURE_2D (назовем его текстурой 2D). Обе эти сгенерированные текстуры в начале пусты. При связывании с MediaPlayer текстура OES будет обновляться во время воспроизведения видео, тогда мы можем использовать FrameBuffer для рисования содержимого текстуры OES на 2D текстуре.

Я написал версию этого процесса для Android, и он работает очень хорошо, когда я наконец рисую 2D-текстуру на экране. Однако, когда я опубликую его как плагин Unity для Android и запустит тот же код на Unity, не будет никаких изображений. Вместо этого отображает только предустановленный цвет от glClearColor, что означает две вещи:

  1. Процесс передачи текстуры OES -> FrameBuffer -> 2D текстуры завершен, и Unity получает окончательную 2D текстуру. Потому что glClearColor вызывается только тогда, когда мы рисуем содержимое текстуры OES в FrameBuffer.
  2. Есть некоторые ошибки во время рисования после glClearColor, потому что мы не видим изображения видеокадров. На самом деле, я также вызываю glReadPixels после рисования и перед отменой привязки к FrameBuffer, который будет считывать данные из FrameBuffer, с которым мы связаны. И он возвращает значение одного цвета, совпадающее с цветом, который мы установили в glClearColor.

Чтобы упростить код, который я должен предоставить здесь, я собираюсь нарисовать треугольник для 2D текстуры через FrameBuffer. Если мы сможем выяснить, какая часть неправильна, то мы легко сможем решить аналогичную проблему для рисования видеокадров.

Функция будет вызываться в 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();
  }

Инициализация 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() из 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);
  }

вершинный шейдер Triangle:

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

фрагментный шейдер Triangle:

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

Код ключа 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);
  }

И, наконец, некоторый код на стороне 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;

Ну, код выше не будет отображать ожидаемый треугольник, а только синий фон. Я добавляю glGetError после почти каждого вызова функций OpenGL, пока не выдается никаких ошибок.

Моя версия Unity 2017.2.1. Для сборки Android я отключил экспериментальный многопоточный рендеринг, и все остальные настройки по умолчанию (без сжатия текстур, не использовать сборку разработки и т. Д.). Минимальный уровень API моего приложения - 5,0 Lollipop, а целевой уровень API - 9,0 шт.

Мне действительно нужна помощь, заранее спасибо!

1 Ответ

0 голосов
/ 10 сентября 2018

Теперь я нашел ответ: Если вы хотите сделать какие-либо задания рисования в вашем плагине, вы должны сделать это на собственном слое . Поэтому, если вы хотите создать плагин для Android, вы должны вызывать API OpenGL-ES на JNI вместо Java. Причина в том, что Unity позволяет рисовать графику только в потоке визуализации. Если вы просто вызываете API-интерфейсы OpenGL-ES, как я делал на стороне Java, как в описании вопроса, они фактически будут работать в основном потоке Unity вместо потока рендеринга. Unity предоставляет метод, GL.IssuePluginEvent, для вызова ваших собственных функций при рендеринге потока, но ему нужно собственное кодирование, так как эта функция требует указателя функции в качестве обратного вызова. Вот простой пример его использования:

На JNI сторона:

// 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;
}

На стороне Android Java:

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

На стороне Единства:

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
}

Вам все еще нужно перенести текстуру, и все об этом должно происходить на JNI слое. Но не волнуйтесь, они почти такие же, как я делал в описании вопроса, но только на другом языке, нежели Java, и есть много материалов об этом процессе, так что вы наверняка сможете его сделать.

Наконец, позвольте мне обратиться к ключу, чтобы решить эту проблему еще раз: делать свои нативные вещи на родном уровне и не зависеть от чистой Java ... Я полностью удивлен, что нет блогов / answer / wiki, чтобы сказать нам, просто напишите наш код на C ++. Хотя есть некоторые реализации с открытым исходным кодом, такие как Google gvr-unity-sdk, они дают полную справку, но вы все равно будете сомневаться, что, возможно, вы сможете завершить задачу, не написав никакого кода на C ++. Теперь мы знаем, что не можем. Однако, если честно, я думаю, что у Unity есть возможность сделать этот прогресс еще проще.

...