Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示

Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示

目录

[Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示](#Unity 之 【Android Unity 共享纹理】之 Android 共享图片给 Unity 显示)

一、简单介绍

二、共享纹理

1、共享纹理的原理

2、共享纹理涉及到的关键知识点

3、什么可以实现共享

不能实现共享的情况

4、共享纹理中的注意事项

三、注意事项

四、效果预览

[五、简单实现 Android 共享图片给 Unity 显示 案例](#五、简单实现 Android 共享图片给 Unity 显示 案例)

1、案例环境

[2、Android 端](#2、Android 端)

[3、Unity 端](#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),并在不同的渲染环境中使用同一个纹理对象。主要涉及以下几个步骤:

  1. 创建共享的EGL上下文

    • EGL(Embedded-System Graphics Library)用于管理图形上下文和绘图表面。通过EGL,可以创建一个共享的上下文,使得不同的线程可以访问同一个纹理。
  2. 生成OpenGL纹理

    • 在共享的EGL上下文中生成一个OpenGL纹理对象。
  3. 在Unity中使用该纹理

    • 在Unity中通过Texture2D.CreateExternalTexture方法创建一个引用共享纹理的Texture2D对象,从而在Unity的渲染环境中使用该纹理。

2、共享纹理涉及到的关键知识点

  1. EGLContext

    • EGLContext是EGL用于管理OpenGL ES图形上下文的对象。通过创建共享的EGLContext,可以在不同的线程之间共享OpenGL资源。
  2. EGLSurface

    • EGLSurface是EGL用于表示绘图表面的对象。可以是窗口表面、Pbuffer(像素缓冲区)表面或屏幕外渲染表面。
  3. OpenGL ES

    • OpenGL ES是OpenGL的一个子集,专门用于嵌入式系统(如移动设备)。在共享纹理的过程中,OpenGL ES提供了创建和操作纹理的API。
  4. Texture2D.CreateExternalTexture

    • Unity中的方法,用于创建一个引用外部纹理的Texture2D对象。这是将共享的OpenGL纹理引入Unity渲染环境的关键步骤。

3、什么可以实现共享

  • OpenGL纹理:可以通过共享的EGLContext在不同的线程或进程中共享。
  • Pbuffer Surface:可以用于共享离屏渲染的纹理。
不能实现共享的情况
  • 不同设备之间:共享纹理通常仅限于同一设备上的不同进程或线程之间。
  • 非OpenGL资源:非OpenGL的图形资源,如CPU内存中的图像数据,不能直接通过EGL共享。

4、共享纹理中的注意事项

  1. 同步问题

    • 在多线程环境中使用共享纹理时,需要注意同步问题,避免不同线程同时对同一纹理进行读写操作,导致数据竞争或一致性问题。
  2. 上下文管理

    • 确保正确管理EGLContext和EGLSurface的生命周期。在创建和销毁上下文时,需要注意资源的正确释放。
  3. 性能优化

    • 共享纹理可以减少数据复制,提高性能,但仍需注意渲染流程中的其他瓶颈,进行整体的性能优化。
  4. 兼容性

    • 不同设备和操作系统版本可能对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环境,创建和管理纹理。EGL14GLES20是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。
    • mTextureWidthmTextureHeight: 纹理的宽度和高度。
    • 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,用于创建外部纹理。
    • mWidthmHeight: 存储纹理的宽度和高度。
  • 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

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

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安卓共享纹理

相关推荐
LSL666_18 小时前
5 Repository 层接口
android·运维·elasticsearch·jenkins·repository
alexhilton21 小时前
在Jetpack Compose中创建CRT屏幕效果
android·kotlin·android jetpack
2501_940094021 天前
emu系列模拟器最新汉化版 安卓版 怀旧游戏模拟器全集附可运行游戏ROM
android·游戏·安卓·模拟器
下位子1 天前
『OpenGL学习滤镜相机』- Day9: CameraX 基础集成
android·opengl
参宿四南河三1 天前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我1 天前
mmkv的 mmap 的理解
android
没有了遇见1 天前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong1 天前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强1 天前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸1 天前
【底层机制】 Android ION内存分配器深度解析
android·面试