Android 上的 OpenGL 绘图结合 Unity 通过帧缓冲区传输纹理无法工作
OpenGL drawing on Android combining with Unity to transfer texture through frame buffer cannot work
我目前正在为 Unity 制作 Android 播放器插件。基本想法是我将通过 MediaPlayer
在 Android 上播放视频,它提供 setSurface
API 接收 SurfaceTexture
作为构造函数参数,最后与 OpenGL-ES 纹理绑定。在大多数其他情况下,如显示图像,我们可以将此纹理以 pointer/id 的形式发送到 Unity,在那里调用 Texture2D.CreateExternalTexture
以生成一个 Texture2D
对象并将其设置为 UI GameObject
渲染图片。然而,在显示视频帧时,它有点不同,因为 在 Android 上播放的视频需要类型 GL_TEXTURE_EXTERNAL_OES
的纹理,而 Unity 仅支持通用类型 GL_TEXTURE_2D
.
为了解决这个问题,我google了一会儿,知道我应该采用一种叫做"Render to texture"的技术。更明确地说,我应该生成 2 个纹理,一个用于 Android 中的 MediaPlayer
和 SurfaceTexture
以接收视频帧,另一个用于 Unity,其中也应该包含图片数据。第一个应该是 GL_TEXTURE_EXTERNAL_OES
的类型(我们简称为 OES 纹理),第二个是 GL_TEXTURE_2D
的类型(我们称之为 2D 纹理)。这两个生成的纹理在开始时都是空的。当绑定MediaPlayer
时,OES贴图会在视频播放时更新,然后我们可以使用FrameBuffer
将OES贴图的内容绘制到2D贴图上。
我已经编写了此过程的纯Android 版本,当我最终在屏幕上绘制 2D 纹理时,它运行良好。但是,当我将其发布为 Unity Android 插件并在 Unity 上运行相同的代码时,不会显示任何图片。相反,它只显示来自 glClearColor
的预设颜色,这意味着两件事:
- OES 纹理的传输过程 ->
FrameBuffer
-> 2D 纹理已完成,Unity 确实收到了最终的 2D 纹理。因为glClearColor
只有在我们将OES纹理的内容绘制到FrameBuffer
时才会被调用。
glClearColor
之后的绘图过程中出现了一些错误,因为我们看不到视频帧图片。事实上,我也在绘图之后和与 FrameBuffer
解除绑定之前调用 glReadPixels
,它将从我们绑定的 FrameBuffer
中读取数据。 returns 与我们在 glClearColor
中设置的颜色相同的单色值。
为了简化我应该在这里提供的代码,我将通过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();
}
二维纹理初始化:
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;
好吧,上面的代码不会显示预期的三角形,而只会显示蓝色背景。我在几乎每个 OpenGL 函数调用之后添加 glGetError
而没有抛出任何错误。
我的Unity版本是2017.2.1。对于 Android 构建,我关闭了实验性多线程渲染,其他设置都是默认设置(无纹理压缩,不使用开发构建等)。我的应用程序的最低 API 级别是 5.0 Lollipop,目标 API 级别是 9.0 Pie。
我真的需要一些帮助,在此先感谢!
现在我找到了答案:如果你想在你的插件中做任何绘图工作,你应该在原生层做它。所以如果你想制作一个 Android 插件,你应该在 JNI
而不是 Java 端调用 OpenGL-ES API。原因是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;
}
在AndroidJava侧:
class YourPluginClass {
...
public native long getNativeRenderFunctionPointer();
...
}
在 Unity 端:
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...我很惊讶没有 blog/answer/wiki 告诉我们只用 C++ 编写代码。虽然有一些开源实现,如 Google 的 gvr-unity-sdk,它们提供了完整的参考,但您仍然会怀疑是否可以在不编写任何 C++ 代码的情况下完成任务。现在我们知道我们不能。然而,老实说,我认为 Unity 有能力让这个进步变得更加容易。
我目前正在为 Unity 制作 Android 播放器插件。基本想法是我将通过 MediaPlayer
在 Android 上播放视频,它提供 setSurface
API 接收 SurfaceTexture
作为构造函数参数,最后与 OpenGL-ES 纹理绑定。在大多数其他情况下,如显示图像,我们可以将此纹理以 pointer/id 的形式发送到 Unity,在那里调用 Texture2D.CreateExternalTexture
以生成一个 Texture2D
对象并将其设置为 UI GameObject
渲染图片。然而,在显示视频帧时,它有点不同,因为 在 Android 上播放的视频需要类型 GL_TEXTURE_EXTERNAL_OES
的纹理,而 Unity 仅支持通用类型 GL_TEXTURE_2D
.
为了解决这个问题,我google了一会儿,知道我应该采用一种叫做"Render to texture"的技术。更明确地说,我应该生成 2 个纹理,一个用于 Android 中的 MediaPlayer
和 SurfaceTexture
以接收视频帧,另一个用于 Unity,其中也应该包含图片数据。第一个应该是 GL_TEXTURE_EXTERNAL_OES
的类型(我们简称为 OES 纹理),第二个是 GL_TEXTURE_2D
的类型(我们称之为 2D 纹理)。这两个生成的纹理在开始时都是空的。当绑定MediaPlayer
时,OES贴图会在视频播放时更新,然后我们可以使用FrameBuffer
将OES贴图的内容绘制到2D贴图上。
我已经编写了此过程的纯Android 版本,当我最终在屏幕上绘制 2D 纹理时,它运行良好。但是,当我将其发布为 Unity Android 插件并在 Unity 上运行相同的代码时,不会显示任何图片。相反,它只显示来自 glClearColor
的预设颜色,这意味着两件事:
- OES 纹理的传输过程 ->
FrameBuffer
-> 2D 纹理已完成,Unity 确实收到了最终的 2D 纹理。因为glClearColor
只有在我们将OES纹理的内容绘制到FrameBuffer
时才会被调用。 glClearColor
之后的绘图过程中出现了一些错误,因为我们看不到视频帧图片。事实上,我也在绘图之后和与FrameBuffer
解除绑定之前调用glReadPixels
,它将从我们绑定的FrameBuffer
中读取数据。 returns 与我们在glClearColor
中设置的颜色相同的单色值。
为了简化我应该在这里提供的代码,我将通过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();
}
二维纹理初始化:
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;
好吧,上面的代码不会显示预期的三角形,而只会显示蓝色背景。我在几乎每个 OpenGL 函数调用之后添加 glGetError
而没有抛出任何错误。
我的Unity版本是2017.2.1。对于 Android 构建,我关闭了实验性多线程渲染,其他设置都是默认设置(无纹理压缩,不使用开发构建等)。我的应用程序的最低 API 级别是 5.0 Lollipop,目标 API 级别是 9.0 Pie。
我真的需要一些帮助,在此先感谢!
现在我找到了答案:如果你想在你的插件中做任何绘图工作,你应该在原生层做它。所以如果你想制作一个 Android 插件,你应该在 JNI
而不是 Java 端调用 OpenGL-ES API。原因是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;
}
在AndroidJava侧:
class YourPluginClass {
...
public native long getNativeRenderFunctionPointer();
...
}
在 Unity 端:
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...我很惊讶没有 blog/answer/wiki 告诉我们只用 C++ 编写代码。虽然有一些开源实现,如 Google 的 gvr-unity-sdk,它们提供了完整的参考,但您仍然会怀疑是否可以在不编写任何 C++ 代码的情况下完成任务。现在我们知道我们不能。然而,老实说,我认为 Unity 有能力让这个进步变得更加容易。