炒沙作縻终不饱,缕冰文章费工巧。这篇文章主要讲述安卓和Unity线程共享context相关的知识,希望能为你提供帮助。
Unity项目作为library集成到安卓内请看安卓集成Unity开发示例
本项目的目的是实现以下的流程
android和ios原生代码里面操作摄像头-> 获取视频流数据-> 人脸检测或美颜-> 传输给Unity一开始我尝试了最直接的方法,通过
@Override public void onPreviewFrame(byte[] data, Camera camera){
// function trans data[] to Unity
}
但是涉及到格式转换问题此流程非常低效,而且
onPreviewFrame()
方法的回传也涉及到从GPU拷贝到CPU的操作,性能依然不高。文章图片
既然我们的最终目的都是传到GPU上让Unity渲染线程渲染,那能不能直接在GPU层传递纹理数据到Unity呢?
文章图片
完全没有问题,只要我们在Unity线程中拿到EGLContext和EGLConfig,将其作为参数传递给java线程的
eglCreateContext()
创建Java线程的EGLContext,两个线程就可以共享EGLContext了先在安卓端写好获取eglcontext的方法,供Unity调用
// 创建单线程池,用于处理OpenGL纹理
private final ExecutorService mRenderThread = Executors.newSingleThreadExecutor();
public int getStreamTextureWidth() {return mTextureWidth;
}
public int getStreamTextureHeight() {return mTextureHeight;
}
public int getStreamTextureID() {return mTextureID;
}private void glLogE(String msg) {
Log.e(TAG, msg + "
, err="
+ GLES20.glGetError());
} // 被unity调用获取EGLContext
public void setupOpenGL() {
Log.d(TAG, "
setupOpenGL called by Unity "
);
// 获取Unity线程的EGLContext,EGLDisplay
mSharedEglContext = EGL14.eglGetCurrentContext();
if (mSharedEglContext == EGL14.EGL_NO_CONTEXT) {
glLogE("
eglGetCurrentContext failed"
);
return;
}glLogE("
eglGetCurrentContext success"
);
EGLDisplay sharedEglDisplay = EGL14.eglGetCurrentDisplay();
if (sharedEglDisplay == EGL14.EGL_NO_DISPLAY) {
glLogE("
sharedEglDisplay failed"
);
return;
}glLogE("
sharedEglDisplay success"
);
// 获取Unity绘制线程的EGLConfig
int[] numEglConfigs = new int[1];
EGLConfig[] eglConfigs = new EGLConfig[1];
if (!EGL14.eglGetConfigs(sharedEglDisplay, eglConfigs, 0, eglConfigs.length,
numEglConfigs, 0)) {
glLogE("
eglGetConfigs failed"
);
return;
}glLogE("
eglGetConfigs success"
);
mSharedEglConfig = eglConfigs[0];
mRenderThread.execute(new Runnable() {
@Override
public void run() {
// Java线程初始化OpenGL环境
initOpenGL();
// 生成OpenGL纹理ID
int textures[] = new int[1];
GLES20.glGenTextures(1, textures, 0);
if (textures[0] == 0) {
glLogE("
glGenTextures failed"
);
return;
}glLogE("
glGenTextures success"
);
mTextureID = textures[0];
mTextureWidth = 670;
mTextureHeight = 670;
}
});
}
Java线程在此之后初始化OpenGL环境
private void initOpenGL() {
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
glLogE("
eglGetDisplay failed"
);
return;
}glLogE("
eglGetDisplay success"
);
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
mEGLDisplay = null;
glLogE("
eglInitialize failed"
);
return;
}glLogE("
eglInitialize success"
);
int[] eglContextAttribList = new int[]{
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, // 版本需要与Unity使用的一致
EGL14.EGL_NONE
};
// 将Unity线程的EGLContext和EGLConfig作为参数,传递给eglCreateContext,
// 创建Java线程的EGLContext,从而实现两个线程共享EGLContext
mEglContext = EGL14.eglCreateContext(
mEGLDisplay, mSharedEglConfig, mSharedEglContext,
eglContextAttribList, 0);
if (mEglContext == EGL14.EGL_NO_CONTEXT) {
glLogE("
eglCreateContext failed"
);
return;
}glLogE("
eglCreateContext success"
);
int[] surfaceAttribList = {
EGL14.EGL_WIDTH, 64,
EGL14.EGL_HEIGHT, 64,
EGL14.EGL_NONE
};
// Java线程不进行实际绘制,因此创建PbufferSurface而非WindowSurface
// 将Unity线程的EGLConfig作为参数传递给eglCreatePbufferSurface
// 创建Java线程的EGLSurface
mEglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mSharedEglConfig, surfaceAttribList, 0);
if (mEglSurface == EGL14.EGL_NO_SURFACE) {
glLogE("
eglCreatePbufferSurface failed"
);
return;
}glLogE("
eglCreatePbufferSurface success"
);
if (!EGL14.eglMakeCurrent(mEGLDisplay, mEglSurface, mEglSurface, mEglContext)) {
glLogE("
eglMakeCurrent failed"
);
return;
}glLogE("
eglMakeCurrent success"
);
GLES20.glFlush();
}
共享context后,两个线程就可以共享纹理了。将Java线程生成的纹理id返回给Unity线程即可,C#代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class GLTexture : MonoBehaviour
{
private AndroidJavaObject mGLTexCtrl;
private int mTextureId;
private int mWidth;
private int mHeight;
private void Awake(){
// 实例化com.xxx.nativeandroidapp.GLTexture类的对象
mGLTexCtrl = new AndroidJavaObject("
com.xxx.nativeandroidapp.GLTexture"
);
// 初始化OpenGL
mGLTexCtrl.Call("
setupOpenGL"
);
}void Start(){
BindTexture();
}void BindTexture(){
// 获取JavaPlugin生成的纹理ID
mTextureId = mGLTexCtrl.Call<
int>
("
getStreamTextureID"
);
if (mTextureId == 0){
Debug.LogError("
getStreamTextureID failed"
);
return;
} Debug.Log("
getStreamTextureID success"
);
mWidth = mGLTexCtrl.Call<
int>
("
getStreamTextureWidth"
);
mHeight = mGLTexCtrl.Call<
int>
("
getStreamTextureHeight"
);
// 将纹理ID与当前GameObject绑定
material.mainTexture = Texture2D.CreateExternalTexture(mWidth, mHeight, TextureFormat.ARGB32, false, false, (IntPtr)mTextureId);
// 更新纹理数据
mGLTexCtrl.Call("
updateTexture"
);
}
}
unity需要调用updateTexture方法更新纹理
public void updateTexture() {
//Log.d(TAG,"
updateTexture called by unity"
);
mRenderThread.execute(new Runnable() {
@Override
public void run() {
String imageFilePath = "
your own picture path"
;
//图片路径
final Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
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);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
bitmap.recycle();
//回收内存
}
});
}
同时注意必须关闭unity的多线程渲染,否则无法获得Unity渲染线程的EGLContext:
文章图片
然后就可以将Unity工程打包到安卓项目,如果没意外是可以显示纹理到cube等3Dobject上的
文章图片
上述的方案假定Java层更新纹理时使用的是RGB或RBGA格式的数据,但是播放视频或者camera预览这种应用场景下,解码器解码出来的数据如果是YUV格式,渲染起来就稍微麻烦点,考虑到性能需要使用GPU进行转换,可以编写Shader来实现。
如前文所述,Unity只需要Java层的纹理ID,当使用Shader进行YUV转RGB时,怎么实现更新该纹理的数据呢?答案是Render to Texture。具体做法是,创建一个FrameBuffer,调用glFramebufferTexture2D将纹理与FrameBuffer关联起来,这样在FrameBuffer上进行的绘制,就会被写入到该纹理中。
我们先新建TextureSurface用于接收摄像头的数据
mCameraInputSurface = new SurfaceTexture(0);
mCameraInputSurface.setOnFrameAvailableListener(this);
mCameraInputSurface.setDefaultBufferSize(mFrameWidth, mFrameHeight);
mOutputSurfaceTexture.setOnFrameAvailableListener(this);
设置摄像机的预览到这个Surface,并开始预览
mCamera.setPreviewTexture(mCameraInputSurface);
mCamera.startPreview();
然后这个SurfaceTexture就可以跨线程共享硬件纹理数据了。
【安卓和Unity线程共享context】和之前一样,在u3d的主线程中获取OpenGL的共享Context,用这个context创建共享线程,然后这个线程对
mOutputTex
进行绘制。先用这个mOutputTex
绑定好FBO,然后把上面mCameraInputSurface
的图像渲染到FBO中,最后通知u3d的主线程,可以使用这张纹理进行渲染了。这里的坑之后再填。推荐阅读
- ERROR: for pigfarm-appCannot start service pigfarm: OCI runtime create failed: container_linux.go:
- android开发Activity启动流程简单记录方便搜索以及回顾
- uniapp微信小程序canvas隐藏
- create-react-app + Typescript脚手架搭建
- h5网页打开手机APP
- 启动appium server时打印日志时间
- AndroidStudio 离线安装配置
- 300个核心Java面试问题和答案(一)
- 34个Java集合面试题和答案汇总