Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示
目录
Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示
一、简单介绍
二、共享纹理
1、共享纹理的原理
2、共享纹理涉及到的关键知识点
3、什么可以实现共享
不能实现共享的情况
4、共享纹理中的注意事项
三、注意事项
四、效果预览
五、简单实现 Android 共享图片给 Unity 显示 案例
1、案例环境
2、Android 端
3、Unity 端
六、关键代码
一、简单介绍
Unity 是一个功能强大的跨平台游戏引擎,广泛用于开发视频游戏和其他实时3D互动内容,如模拟器和虚拟现实应用。
游戏引擎:
- Unity:Unity Technologies 开发的跨平台游戏引擎,支持2D和3D图形、物理引擎、音频、视频、网络和多平台发布。
- 跨平台支持:Unity 支持在多个平台上发布,包括 Windows、macOS、Linux、iOS、Android、WebGL、PlayStation、Xbox、Switch 等。
开发环境:
- Unity Editor:用于创建和管理 Unity 项目的集成开发环境(IDE)。开发者可以在其中创建场景、设计关卡、编写代码和调试游戏。
- 场景(Scene):Unity 中的基本构建块,一个场景可以被视为一个关卡或一个游戏中的独立部分。
编程语言:
- C#:主要编程语言。Unity 使用 C# 脚本来控制游戏对象和实现游戏逻辑。
- UnityScript(已弃用):曾经支持的 JavaScript 变种,但已经被弃用。
Unity 进阶开发涉及更复杂的技术和更深入的知识,以创建高性能、复杂和专业的游戏和应用程序。以下是一些 Unity 进阶开发的关键领域和技术:
设计模式:
- 学习和应用常见的设计模式(如单例模式、工厂模式、观察者模式)以便更好地组织和管理代码。
异步编程:
- 使用 C# 的 async 和 await 关键字进行异步编程,以提高应用的响应速度和性能。
依赖注入:
- 使用依赖注入(如 Zenject)来管理对象的依赖关系,减少耦合,提高代码的可测试性和可维护性。
二、共享纹理
共享纹理指的是在不同的图形处理环境(例如 Unity 和 Android 原生代码)之间共享图像数据,而不需要进行图像数据的复制。这样可以大大提升性能,减少内存使用,因为避免了重复加载和处理同一图像数据的开销。
1、共享纹理的原理
共享纹理的基本原理是通过创建一个共享的图形上下文(context),并在不同的渲染环境中使用同一个纹理对象。主要涉及以下几个步骤:
创建共享的EGL上下文:
- EGL(Embedded-System Graphics Library)用于管理图形上下文和绘图表面。通过EGL,可以创建一个共享的上下文,使得不同的线程可以访问同一个纹理。
生成OpenGL纹理:
- 在共享的EGL上下文中生成一个OpenGL纹理对象。
在Unity中使用该纹理:
- 在Unity中通过
Texture2D.CreateExternalTexture
方法创建一个引用共享纹理的Texture2D对象,从而在Unity的渲染环境中使用该纹理。
2、共享纹理涉及到的关键知识点
EGLContext:
- EGLContext是EGL用于管理OpenGL ES图形上下文的对象。通过创建共享的EGLContext,可以在不同的线程之间共享OpenGL资源。
EGLSurface:
- EGLSurface是EGL用于表示绘图表面的对象。可以是窗口表面、Pbuffer(像素缓冲区)表面或屏幕外渲染表面。
OpenGL ES:
- OpenGL ES是OpenGL的一个子集,专门用于嵌入式系统(如移动设备)。在共享纹理的过程中,OpenGL ES提供了创建和操作纹理的API。
Texture2D.CreateExternalTexture:
- Unity中的方法,用于创建一个引用外部纹理的Texture2D对象。这是将共享的OpenGL纹理引入Unity渲染环境的关键步骤。
3、什么可以实现共享
- OpenGL纹理:可以通过共享的EGLContext在不同的线程或进程中共享。
- Pbuffer Surface:可以用于共享离屏渲染的纹理。
不能实现共享的情况
- 不同设备之间:共享纹理通常仅限于同一设备上的不同进程或线程之间。
- 非OpenGL资源:非OpenGL的图形资源,如CPU内存中的图像数据,不能直接通过EGL共享。
4、共享纹理中的注意事项
同步问题:
- 在多线程环境中使用共享纹理时,需要注意同步问题,避免不同线程同时对同一纹理进行读写操作,导致数据竞争或一致性问题。
上下文管理:
- 确保正确管理EGLContext和EGLSurface的生命周期。在创建和销毁上下文时,需要注意资源的正确释放。
性能优化:
- 共享纹理可以减少数据复制,提高性能,但仍需注意渲染流程中的其他瓶颈,进行整体的性能优化。
兼容性:
- 不同设备和操作系统版本可能对EGL和OpenGL ES的支持有所不同。在实现共享纹理时,需要考虑设备兼容性问题,确保在目标设备上正常运行。
三、注意事项
1、一定要注意 Unity 端设置的 多线程渲染 关闭掉,以及 移除 Vulkan 配置,不然会报错,创建共享失败,共享id 会是 0
2、共享图片的可能会翻转,注意合理调整
四、效果预览
五、简单实现 Android 共享图片给 Unity 显示 案例
1、案例环境
- Windows 10
- Android Studio Dolphin | 2021.3.1 Patch 1
- Unity 2021.3.16f
2、Android 端
该案例中使用的OpenGL和EGL函数主要用于设置OpenGL环境,创建和管理纹理。EGL14
和GLES20
是Android的OpenGL ES 1.4和2.0的API。这段代码是Android原生代码与Unity集成的一个例子,允许Unity访问和操作OpenGL纹理。
该类封装了OpenGL ES 2.0的初始化和纹理管理,其主要包括下面参数与函数:
1、成员变量:
mSharedEglContext
: 共享的EGL上下文,从Unity线程获取。mSharedEglConfig
: 共享的EGL配置。mTextureID
: OpenGL纹理ID。mTextureWidth
和mTextureHeight
: 纹理的宽度和高度。mRenderThread
: 一个执行服务,用于在单独的线程上执行渲染任务。2、构造函数 (
public NativeAndroidApp()
): 初始化一个单线程的执行服务,用于执行OpenGL的初始化和渲染任务。3、setupOpenGL: 这个方法被Unity调用,用于设置OpenGL环境。它执行以下步骤:
- 获取当前Unity线程的EGL上下文和显示。
- 获取Unity绘制线程的EGL配置。
- 在渲染线程上初始化OpenGL环境,并生成OpenGL纹理ID。
4、initOpenGL: 私有方法,用于初始化OpenGL环境。它包括:
- 获取默认的EGL显示。
- 初始化EGL,获取版本信息。
- 创建EGL上下文,指定OpenGL ES 2.0。
- 创建一个像素缓冲区表面(PbufferSurface)。
- 将EGL上下文和表面设置为当前。
5、getStreamTextureID, getStreamTextureWidth, getStreamTextureHeight: 这些公共方法被Unity调用,用于获取纹理ID和尺寸信息。
6、updateTexture: 这个方法被Unity调用以更新纹理内容。虽然这个方法的实现在代码片段中没有给出,但它可能涉及将新的图像数据绑定到OpenGL纹理,并使用
glTexImage2D
等函数更新纹理。
1)打开 Android Studio ,创建一个 Android Library
2) 然后,添加一张测试图片,来作为共享
3)然后创建建一个脚本,编写对应函数实现共享纹理图片的功能
4)接着 ‘Build-Make Module 'xxxxxx'’ 打包成 aar
3、Unity 端
实现Unity与Android原生代码之间的纹理共享。它通过调用Android端的Java代码来获取和更新纹理数据,并在Unity中显示这些纹理。以下是脚本的主要功能和关键函数的说明,脚本通过与Android原生层交互,实现了Unity与Android之间的纹理共享:
1、成员变量:
mGLTexCtrl
:AndroidJavaObject
类型,用于调用Java层的方法。mTexture2D
:Texture2D
类型,Unity中的纹理对象,用于在Unity中显示纹理。mTextureId
: 存储从Java层获取的纹理ID,用于创建外部纹理。mWidth
和mHeight
: 存储纹理的宽度和高度。2、Awake函数: 在Unity对象初始化时调用,用于实例化
AndroidJavaObject
并调用Java层的setupOpenGL
方法来初始化OpenGL环境。3、Start函数: 在Unity对象启动时调用,用于绑定纹理到UI组件
RawImage
。4、BindTexture函数:
- 调用Java层的方法获取纹理ID、宽度和高度。
- 使用获取到的纹理ID创建Unity的
Texture2D
对象,格式为RGBA32
,不具有Mipmaps,不使用Readable设置。- 将创建的纹理赋值给
RawImage
组件的texture
属性,以便在UI上显示。5、Update函数: 每帧调用,用于调用
UpddateTextureate
方法更新纹理数据。6、UpddateTextureate函数:
- 调用Java层的
updateTexture
方法来更新纹理数据。- 这个方法应该在每帧或者在特定条件下调用,以确保纹理内容是最新的。
7、OnDestroy函数: 当Unity对象被销毁时调用,用于释放纹理资源,避免内存泄漏。
1)创建一个 Unity 工程,创建文件夹 Plugins-Android ,把之前的生成的 aar 添加到文件夹中
2)在场景中添加一个 RawImage ,用来显示共享的纹理图片
3)创建一个脚本,调用 Android 端封装暴露的纹理共享接口
4)把创建的脚本挂载到场景中,对应赋值 RawImage
5)把平台切换到 Android ,对应关闭 设置中的多线程渲染,以及 移除 Vulkan
6、打包,在设备上运行,效果如下
7)可能你会发现图片反了,你可以Android端修改,也可以 Unity端修改,这里不在赘述,最后调整效果如下
六、关键代码
1、TextureShareDemo.java
package com.ffalcon.unitytexturesharemodule;
import static android.opengl.GLES20.glGetError;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 共享纹理 Demo
*/
public class TextureShareDemo {
/**
* 用于日志输出的标签
*/
private static final String TAG = "[TextureShareDemo] ";
/**
* 共享的EGL上下文,从Unity线程获取
*/
private EGLContext mSharedEglContext;
/**
* 共享的EGL配置
*/
private EGLConfig mSharedEglConfig;
/**
* OpenGL纹理ID
*/
private int mTextureID;
/**
* 纹理宽度
*/
private int mTextureWidth;
/**
* 纹理高度
*/
private int mTextureHeight;
/**
* 用于OpenGL渲染操作的线程池
*/
private ExecutorService mRenderThread;
/**
* 构造函数,初始化线程池
*/
public TextureShareDemo() {
mRenderThread = Executors.newSingleThreadExecutor();
}
/**
* 由Unity调用,用于设置OpenGL环境
* 此方法在Unity线程执行
*/
public void setupOpenGL() {
Log.d(TAG, "setupOpenGL called by Unity");
// 获取Unity线程当前的EGL上下文和显示
mSharedEglContext = EGL14.eglGetCurrentContext();
EGLDisplay sharedEglDisplay = EGL14.eglGetCurrentDisplay();
// 获取Unity绘制线程的EGL配置
int[] numEglConfigs = new int[1];
EGLConfig[] eglConfigs = new EGLConfig[1];
if (!EGL14.eglGetConfigs(sharedEglDisplay, eglConfigs, 0, eglConfigs.length, numEglConfigs, 0)) {
Log.e(TAG, "eglGetConfigs failed");
return;
}
mSharedEglConfig = eglConfigs[0];
// 在渲染线程上执行OpenGL初始化和纹理创建
mRenderThread.execute(new Runnable() {
@Override
public void run() {
initOpenGL();
// 生成OpenGL纹理ID
int textures[] = new int[1];
GLES20.glGenTextures(1, textures, 0);
if (textures[0] == 0) {
Log.e(TAG, "glGenTextures failed");
return;
}
mTextureID = textures[0];
// 这里设置的纹理尺寸是示例值
mTextureWidth = 670;
mTextureHeight = 670;
}
});
}
/**
* 初始化OpenGL环境
*/
private void initOpenGL() {
Log.d(TAG, "initOpenGL: ");
EGLDisplay mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
int[] version = new int[2];
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
Log.e(TAG, "Failed to initialize EGL.");
return;
}
// 创建EGL上下文,指定OpenGL ES 3版本
int[] eglContextAttribList = new int[]{
EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
EGL14.EGL_NONE
};
EGLContext mEglContext = EGL14.eglCreateContext(
mEGLDisplay, mSharedEglConfig, mSharedEglContext, eglContextAttribList, 0);
if (mEglContext == EGL14.EGL_NO_CONTEXT) {
Log.e(TAG, "Failed to create EGL context.");
return;
}
// 创建像素缓冲区表面
int[] surfaceAttribList = {
EGL14.EGL_WIDTH, 64,
EGL14.EGL_HEIGHT, 64,
EGL14.EGL_NONE
};
EGLSurface mEglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mSharedEglConfig, surfaceAttribList, 0);
if (mEglSurface == EGL14.EGL_NO_SURFACE) {
Log.e(TAG, "Failed to create EGL surface.");
return;
}
// 设置EGL上下文和表面为当前
if (!EGL14.eglMakeCurrent(mEGLDisplay, mEglSurface, mEglSurface, mEglContext)) {
Log.e(TAG, "Failed to make EGL context current.");
return;
}
GLES20.glFlush();
// 检查OpenGL是否有错误发生
int error = glGetError();
if (error != GLES20.GL_NO_ERROR) {
Log.e(TAG, "OpenGL error: " + error);
}
}
/**
* 获取纹理ID
* @return 纹理ID
*/
public int getStreamTextureID() {
Log.d(TAG, "getStreamTextureID: mTextureID = " + mTextureID);
return mTextureID;
}
/**
* 获取纹理宽度
* @return 纹理宽度
*/
public int getStreamTextureWidth() {
Log.d(TAG, "getStreamTextureWidth: mTextureWidth = " + mTextureWidth);
return mTextureWidth;
}
/**
* 获取纹理高度
* @return 纹理高度
*/
public int getStreamTextureHeight() {
Log.d(TAG, "getStreamTextureHeight: mTextureHeight = " + mTextureHeight);
return mTextureHeight;
}
/**
* 更新纹理内容
* 此方法应在渲染线程中调用
*/
public void updateTexture() {
mRenderThread.execute(new Runnable() {
@Override
public void run() {
// 从资源中加载位图
final Bitmap bitmap = BitmapFactory.decodeResource(getUnityContext().getResources(), R.drawable.wutiaowu);
if (bitmap != null) {
// 绑定纹理ID
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);
// 设置纹理参数
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, 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);
// 将位图数据上传到纹理
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// 解绑纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
// 回收位图占用的内存
bitmap.recycle();
} else {
// 如果位图加载失败,记录错误日志
android.util.Log.e(TAG, "Failed to load bitmap.");
}
}
});
}
// region 获取Unity的Activity和Context
/**
* 设置一个 Activity 参数
*/
protected static Activity unityActivity;
/**
* 通过反射获取Unity的Activity
* @return Unity的Activity
*/
protected static Activity getActivity() {
// 反射获取当前Activity,如果尚未获取则尝试通过UnityPlayer类获取
if (null == unityActivity) {
try {
Class<?> classtype = Class.forName("com.unity3d.player.UnityPlayer");
unityActivity = (Activity) classtype.getDeclaredField("currentActivity").get(classtype);
} catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
return unityActivity;
}
/**
* 获取Unity的上下文Context
* @return Unity的上下文Context
*/
protected static Context getUnityContext(){
return getActivity().getApplicationContext();
}
//endregion
}
2、TextureShareDemo.cs
using System;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Android Unity 共享纹理 demo
/// </summary>
public class TextureShareDemo : MonoBehaviour
{
// 用于日志输出的标签
const string TAG = "[UnityNativeTexture] ";
// AndroidJavaObject对象,用于与Java代码交互
private AndroidJavaObject mGLTexCtrl;
// Unity中的Texture2D对象,用于显示纹理
private Texture2D mTexture2D;
// 纹理ID,用于创建外部纹理
private int mTextureId = 0;
// 纹理的宽度
private int mWidth;
// 纹理的高度
private int mHeight;
// Unity UI组件RawImage,用于显示纹理
public RawImage RawImage;
/// <summary>
/// 在Unity初始化时调用
/// </summary>
void Awake()
{
// 创建AndroidJavaObject实例,与Java层的TextureShareDemo类交互
// 这里的包名和类名应与实际Java类名一致
mGLTexCtrl = new AndroidJavaObject("com.ffalcon.unitytexturesharemodule.TextureShareDemo");
// 调用Java层的方法以初始化OpenGL上下文
mGLTexCtrl.Call("setupOpenGL");
}
/// <summary>
/// 在Unity开始时调用
/// </summary>
void Start()
{
// 绑定纹理到RawImage组件
BindTexture();
}
/// <summary>
/// 绑定纹理的方法
/// </summary>
void BindTexture()
{
Debug.Log(TAG + "BindTexture : ");
// 从Java层获取纹理ID、宽度和高度
mTextureId = mGLTexCtrl.Call<int>("getStreamTextureID");
mWidth = mGLTexCtrl.Call<int>("getStreamTextureWidth");
mHeight = mGLTexCtrl.Call<int>("getStreamTextureHeight");
// 检查纹理ID是否有效
if (mTextureId != 0)
{
Debug.Log(TAG + "BindTexture : mTextureId = " + mTextureId);
// 使用获取到的纹理ID创建外部纹理
mTexture2D = Texture2D.CreateExternalTexture(mWidth, mHeight, TextureFormat.RGBA32, false, false, (IntPtr)mTextureId);
// 将创建的纹理赋值给RawImage组件的texture属性
RawImage.texture = mTexture2D;
}
else
{
// 如果纹理ID无效,记录错误日志
Debug.LogError(TAG + "BindTexture : Failed to get valid texture ID from Android.");
}
}
/// <summary>
/// 每帧调用的Update方法
/// </summary>
private void Update()
{
// 调用UpdateTextureate方法来更新纹理数据
UpddateTextureate();
}
/// <summary>
/// 更新纹理数据的方法
/// </summary>
void UpddateTextureate()
{
// 这里应该是一个拼写错误,正确的方法名应该是UpdateTexture
// 调用Java层的方法来更新纹理数据
if (mGLTexCtrl != null && mTextureId != 0)
{
Debug.Log(TAG + "Update : mGLTexCtrl.Call(\"updateTexture\") mTextureId = " + mTextureId);
mGLTexCtrl.Call("updateTexture");
}
}
/// <summary>
/// 当脚本对象被销毁时调用
/// </summary>
void OnDestroy()
{
// 确保在销毁时释放纹理资源
if (mTexture2D != null)
{
// 释放纹理资源
mTexture2D = null;
}
}
}
参考文献:Unity安卓共享纹理