Android上的OpenGL绘图与Unity结合通过帧缓冲区传输纹理无法工作

Android上的OpenGL绘图与Unity结合通过帧缓冲区传输纹理无法工作,android,unity3d,opengl-es,Android,Unity3d,Opengl Es,我目前正在为Unity制作一个Android播放器插件。基本想法是,我将通过Android上的MediaPlayer播放视频,它提供了一个setSurfaceAPI,接收SurfaceTexture作为构造函数参数,并最终与OpenGL ES纹理绑定。在大多数其他情况下,如显示图像,我们可以将此纹理以指针/id的形式发送到Unity,在那里调用Texture2D.CreateExternalTexture生成Texture2D对象,并将其设置为UIGameObject,以渲染图片。然而,当谈到显

我目前正在为Unity制作一个Android播放器插件。基本想法是,我将通过Android上的
MediaPlayer
播放视频,它提供了一个
setSurface
API,接收
SurfaceTexture
作为构造函数参数,并最终与OpenGL ES纹理绑定。在大多数其他情况下,如显示图像,我们可以将此纹理以指针/id的形式发送到Unity,在那里调用
Texture2D.CreateExternalTexture
生成
Texture2D
对象,并将其设置为UI
GameObject
,以渲染图片。然而,当谈到显示视频帧时,它有点不同,因为在Android上播放视频需要类型为
GL\u texture\u EXTERNAL\u OES
的纹理,而Unity只支持通用类型
GL\u texture\u 2D

为了解决这个问题,我在谷歌搜索了一段时间,知道我应该采用一种叫做“渲染到纹理”的技术。更清楚地说,我应该生成两种纹理,一种用于Android中的
MediaPlayer
SurfaceTexture
来接收视频帧,另一种用于Unity,其中也应该包含图片数据。第一个应为
GL\u纹理\u外部纹理OES
(简称OES纹理)类型,第二个应为
GL\u纹理\u 2D
(简称2D纹理)。这两个生成的纹理在开始时都是空的。当使用
MediaPlayer
绑定时,OES纹理将在视频播放期间更新,然后我们可以使用
FrameBuffer
在2D纹理上绘制OES纹理的内容。

我已经为这个过程编写了一个纯Android版本,当我最终在屏幕上绘制2D纹理时,效果非常好。但是,当我将其发布为Unity Android插件并在Unity上运行相同的代码时,不会显示任何图片。相反,它只显示
glClearColor
中的预设颜色,这意味着两件事:

  • OES纹理->
    FrameBuffer
    ->2D纹理的传输过程已完成,Unity将接收最终的2D纹理。因为只有当我们将OES纹理的内容绘制到
    FrameBuffer
    时,才会调用
    glClearColor
  • glClearColor
    之后的绘图过程中出现了一些错误,因为我们看不到视频帧图片。事实上,在绘图之后和使用
    FrameBuffer
    解除绑定之前,我也调用
    glReadPixels
    ,它将从绑定的
    FrameBuffer
    读取数据。它返回的单色值与我们在
    glClearColor
    中设置的颜色相同
  • 为了简化我应该在这里提供的代码,我将通过
    FrameBuffer
    在2D纹理上绘制一个三角形。如果我们能找出哪一部分是错误的,那么我们就可以很容易地解决类似的问题来绘制视频帧

    该函数将在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()

      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);
      }
    
    attribute vec4 aPosition;
    void main() {
      gl_Position = aPosition;
    }
    
    precision mediump float;
    void main() {
      gl_FragColor = vec4(0.9, 0.0, 0.0, 1.0);
    }
    
    三角形的顶点着色器

      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);
      }
    
    attribute vec4 aPosition;
    void main() {
      gl_Position = aPosition;
    }
    
    precision mediump float;
    void main() {
      gl_FragColor = vec4(0.9, 0.0, 0.0, 1.0);
    }
    
    三角形的碎片着色器

      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);
      }
    
    attribute vec4 aPosition;
    void main() {
      gl_Position = aPosition;
    }
    
    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;
    
    int-textureId=plugin.Call(“displayTriangle”);
    Log(“本机textureId:+textureId”);
    Texture2D triangleTexture=Texture2D.CreateExternalTexture(
    960960,TextureFormat.RGBA32,false,true,(IntPtr)textureId);
    UpdateExternalTexture(triangleTexture.GetNativeTexturePtr());
    rawImage.texture=三角形纹理;
    rawImage.color=color.white;
    
    上面的代码将不会显示预期的三角形,而只显示蓝色背景。我在几乎每次OpenGL函数调用之后添加
    glGetError
    ,而不会抛出错误

    我的Unity版本是2017.2.1。对于Android构建,我关闭了实验性多线程渲染,其他设置都是默认设置(没有纹理压缩,不使用开发构建,等等)。我的应用程序的最低API级别是5.0棒棒糖,目标API级别是9.0派


    我真的需要一些帮助,提前谢谢

    现在我找到了答案:如果你想在插件中进行任何绘图工作,你应该在本机层进行。所以,如果你想制作一个Android插件,你应该在
    JNI
    调用OpenGL ES API,而不是Java端。原因是Unity只允许在其渲染线程上绘制图形。若你们像我在Java端所做的那个样简单地调用OpenGL ES API,就像在问题描述中那个样,它们实际上将在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…<强> >我完全惊讶的是没有博客/答案/ wiki告诉我们在C++中编写我们的代码。虽然有一些开源的实现,比如谷歌的GVR Unity SDK,但是它们提供了一个完整的引用,但是你仍然会怀疑你可以完成任务而不写任何C++代码。现在我们知道我们不能。然而,老实说,我认为团结有能力使这一进展更加容易