Unity 之 【Android Unity FBO渲染】之 [Unity 渲染 Android 端播放的视频] 的一种方法简单整理

Unity 之 【Android Unity FBO渲染】之 [Unity 渲染 Android 端播放的视频] 的一种方法简单整理

目录

[Unity 之 【Android Unity FBO渲染】之 [Unity 渲染 Android 端播放的视频] 的一种方法简单整理](#Unity 之 【Android Unity FBO渲染】之 [Unity 渲染 Android 端播放的视频] 的一种方法简单整理)

一、简单介绍

[二、FBO 简单介绍](#二、FBO 简单介绍)

三、案例实现原理

四、注意事项

五、简单效果预览

六、案例实现步骤

[Android 端:](#Android 端:)

[Unity 端:](#Unity 端:)

七、关键代码


一、简单介绍

Unity作为一款强大的游戏开发引擎,不仅能够处理复杂的游戏逻辑和图形渲染,还具备播放视频的能力。开发者可以直接在Unity编辑器中导入视频文件,或者通过网络流播放视频内容。这些操作通常不需要离开Unity的环境,就可以轻松实现。

然而,在某些特定场景下,开发者可能希望视频的播放逻辑在Android原生代码中实现,尤其是当涉及到直播流或需要使用特定Android直播SDK时。在这种情况下,Unity的角色可能更多地转变为一个视频渲染器,它接收来自Android原生代码的视频帧,并将其显示在屏幕上。这种方式可以利用Android平台的特定优势,比如更好的性能优化或对特定直播协议的支持。

要实现这一目标,我们需要引入一个在图形编程中非常重要的概念:FBO(Frame Buffer Object)。FBO是OpenGL ES中用于渲染操作的对象,它允许开发者创建一个或多个可以在GPU内存中存储像素数据的缓冲区。通过FBO,我们可以将视频帧渲染到一个纹理上,然后将这个纹理作为Unity中的材质传递给渲染管线,最终在Unity的场景中显示出来。

二、FBO 简单介绍

FBO(Frame Buffer Object,帧缓冲对象)是 OpenGL 中用于渲染操作的一种对象,它允许开发者定义自己的渲染目标,而不是默认的帧缓冲(即屏幕,目前主要用于**离屏渲染技术**)。FBO 是一种非常强大的功能,因为它可以用于多种高级渲染技术,如后处理效果、阴影映射、环境贴图(Cubemaps)生成、渲染到纹理等。

FBO 的基本组成:

  1. 颜色附件(Color Attachment):存储渲染操作中的颜色信息。一个 FBO 可以有多个颜色附件,这在多重渲染目标(MRT)中非常有用。

  2. 深度附件(Depth Attachment):存储深度信息,用于深度测试。

  3. 模板附件(Stencil Attachment):存储模板信息,用于模板测试。

  4. 深度-模板附件(Depth-Stencil Attachment):同时存储深度和模板信息。

FBO 的工作流程:

  1. 创建 FBO :通过调用 glGenFramebuffers 函数生成一个或多个 FBO 标识符。

  2. 绑定 FBO :使用 glBindFramebuffer 函数将生成的 FBO 绑定到当前的 OpenGL 上下文中。

  3. 创建并附加纹理 :创建一个或多个纹理对象,并使用 glFramebufferTexture2D(或其他相关函数)将它们附加到 FBO 的颜色、深度或模板附件点。

  4. 创建并附加渲染缓冲(Render Buffer) :对于需要存储深度或模板信息的 FBO,可以创建渲染缓冲对象(Render Buffer),并使用 glFramebufferRenderbuffer 将它们附加到相应的附件点。

  5. 检查完整性 :使用 glCheckFramebufferStatus 检查 FBO 是否配置正确。如果一切正常,应该返回 GL_FRAMEBUFFER_COMPLETE

  6. 渲染到 FBO:在绑定了 FBO 之后,所有的渲染命令都会渲染到 FBO 指定的纹理或渲染缓冲中,而不是默认的帧缓冲。

  7. 使用 FBO 的纹理:一旦渲染完成,可以将 FBO 的颜色附件纹理用作其他渲染操作的输入,例如作为着色器的贴图。

  8. 解绑 FBO :完成渲染后,使用 glBindFramebuffer 将 FBO 解绑,恢复到默认帧缓冲。

FBO本身不是一块内存,没有空间,真正存储东西,可实际读写的是依附于FBO的东西:纹理(texture)和渲染缓存(renderbuffer)。

依附的方式,是一个二维数组(或者说是一个映射表)来管理。

简而言之,FBO(帧缓冲对象)的核心作用是提供一个灵活的框架来管理渲染过程中使用的各种缓冲区。它通过使用一系列的附件点来引用Texture Object(纹理对象)或Renderbuffer Object(渲染缓冲对象),从而实现对渲染目标的精确控制。这些附件点包括但不限于:

  • 颜色缓冲区 :在OpenGL中,可以通过GL_COLOR_ATTACHMENT0(以及其他颜色附件点,如GL_COLOR_ATTACHMENT1等)来引用与FBO相关联的纹理,用于存储渲染操作中的颜色数据。这允许开发者绑定多个纹理到不同的颜色输出,实现多目标渲染(MRT)。

  • 深度缓冲区 :通过GL_DEPTH_ATTACHMENT,FBO可以将深度信息渲染到一个特定的纹理中,这在复杂的渲染场景中尤其有用,比如需要后期处理深度数据以实现某些视觉效果。

  • 模板缓冲区GL_STENCIL_ATTACHMENT用于存储模板数据,这在实现复杂的图形效果,如遮挡和复杂的裁剪操作时非常有用。

这些附件点都是以整数值的形式存在,它们在FBO的上下文中定义了渲染数据应该被存储到哪里。通过这种方式,FBO极大地增强了渲染流程的灵活性和效率,使得开发者可以根据需要定制渲染过程,实现各种高级渲染技术。这种灵活性是FBO在现代图形渲染中不可或缺的一部分,特别是在需要复杂渲染流程的应用程序中。

以**GL_COLOR_ATTCHMENT0**为例子,一个绑定代码如下:

cpp 复制代码
// 1. 将帧缓冲对象(FBO)绑定到当前的绘制环境中。这意味着所有后续的OpenGL ES绘制命令都会将渲染结果输出到这个特定的帧缓冲对象中,而不是默认的帧缓冲(即屏幕)。这一步是设置离屏渲染的关键,它允许我们将渲染内容重定向到一个非显示的纹理中。
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId);

// 2. 将纹理对象绑定到帧缓冲对象的彩色附件点上。在这里,GL_FRAMEBUFFER 指定了我们要操作的帧缓冲对象类型,GL_COLOR_ATTACHMENT0 是帧缓冲对象的颜色附件点,它用于存储渲染操作的颜色数据。GL_TEXTURE_2D 表示我们要绑定的是一个二维纹理对象。unityTextureId 是纹理对象的名称或ID,它是之前通过 glGenTextures 创建并配置好的。0 表示纹理的层级(对于二维纹理,这个值通常是0)。通过这行代码,我们实际上是将FBO的颜色输出定向到了unityTextureId指定的纹理上,这样所有渲染到这个FBO的内容都会被存储在这个纹理中,而不是直接渲染到屏幕上。
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, unityTextureId, 0);

三、案例实现原理

在OpenGL的渲染流程中,帧缓冲对象(FBO)提供了一种机制,允许开发者将渲染输出从默认的帧缓冲(即屏幕)重定向到一个离屏缓冲区。这通常是通过将一个纹理对象绑定到FBO的GL_COLOR_ATTACHMENT0来实现的,正如文中提到的unityTextureId(即是 把一个纹理id叫unityTextureId的纹理,绑定到FBO的GL_COLOR_ATTACHMENT0标签上)。这个过程不仅使得渲染到纹理(Render to Texture)成为可能,而且为复杂的渲染技术打下了基础。

为了实现这一过程,开发者首先需要使用glGenFramebuffers创建一个FBO,并使用glBindFramebuffer将其绑定。接着,通过glGenTextures创建一个纹理,并使用glBindTexture将其绑定。然后,通过设置适当的纹理参数来准备纹理,例如使用glTexParameter来定义纹理的过滤和包装方式。

一旦纹理准备好,就可以使用glFramebufferTexture2D将其绑定到FBO的GL_COLOR_ATTACHMENT0。这一步是将纹理与FBO关联的关键,它指定了渲染操作的目标纹理。在绑定之后,任何对FBO的渲染命令都会将结果写入到unityTextureId指定的纹理中,而不是直接显示在屏幕上。

此外,开发者还可以为FBO配置深度和模板缓冲区,以支持更复杂的渲染场景。这可以通过创建渲染缓冲对象(Renderbuffer)并使用glFramebufferRenderbuffer将其附加到FBO来完成。

完成这些步骤后,FBO就配置好了,可以开始离屏渲染。这意味着,所有的渲染命令都会将内容渲染到unityTextureId纹理中,而这个纹理随后可以在Unity中被用作材质、UI元素或其他图形元素的纹理,实现高度自定义的视觉效果。这种技术在游戏开发和图形应用程序中非常有价值,特别是在需要后处理效果、反射、折射或其他复杂渲染效果时。

基本流程如下:

说明:

  1. 在Android平台上,使用MediaPlayer管理视频播放,输入到 SurfaceTexture, 视频播放可以通过OpenGL ES的帧缓冲对象(FBO)技术进行高效的渲染处理。具体来说,视频播放的输出被直接渲染到一个作为FBO输入的纹理上。这个纹理充当了FBO的颜色附件,捕获了视频的每一帧图像。
  2. 通过对这个纹理进行一系列的图形处理操作,如颜色校正、滤镜效果、或者图像合成等,可以进一步增强视频的视觉表现。这些操作都是在GPU上进行的,利用OpenGL ES的着色器语言(GLSL)编写,确保了处理的高性能和实时性。
  3. 处理后的纹理内容随后被用作Unity中的另一个纹理的数据源。这个纹理是Unity端创建的,它通过特定的接口与Android端的FBO进行数据同步。在Unity中,这个纹理可以被赋予材质,进而应用到3D模型或者UI元素上。
  4. 最终,这个纹理被用作Unity 组件 RawImage/MeshRenderer 的输入,RawImage/MeshRenderer是Unity的组件,用于直接显示纹理内容。这样,视频播放的输出经过一系列的图形处理后,最终在Unity的 RawImage/MeshRenderer 的形式展现给用户,实现了从Android视频播放到Unity UI显示的完整渲染流程。这个过程不仅使得视频内容的展示更加灵活和多样化,也为开发者提供了强大的工具来创造丰富的视觉体验。

四、注意事项

1、目前 Unity 设置中的 Color Space 注意需要设置为 Gamma,不然,可能会报错:

IllegalStateException java.lang.IllegalStateException: Unable to update texture contents

2、目前 Unity 设置中需要取消勾选 Aut Graphics API ,不然,可能会报错:

IllegalStateException java.lang.IllegalStateException: Unable to update texture contents

3、目前 Unity 设置中需要取消勾选 Multithread Rendering,不然,可能会报错:

IllegalStateException java.lang.IllegalStateException: Unable to update texture contents

五、简单效果预览

六、案例实现步骤

在这个案例中,Unity端首先创建了一个Texture2D对象,这是一个用于存储图像或视频帧数据的纹理。创建后,它会生成一个唯一的标识符UnityTextureId,这个ID在Unity和Android之间作为纹理的引用。接着,将这个UnityTextureId传递给Android端。

在Android端,我们利用OpenGL ES的FBO功能,将接收到的UnityTextureId作为纹理绑定到FBO的GL_COLOR_ATTACHMENT0上。这样,Android端的视频播放 MediaPlayer 在渲染视频帧时,就会将每一帧的图像数据渲染到这个纹理中,而不是直接渲染到屏幕上。

在Unity端,通过在Update方法中调用特定的更新函数,可以实时地从Android端获取最新的视频帧数据,并更新Texture2D对象。这样,Unity场景中的材质或UI元素就可以使用这个Texture2D来显示视频内容,实现视频的实时播放。

这个过程允许开发者在Android端利用视频播放 MediaPlayer 进行视频处理,同时在Unity端实现高效的视频显示,确保了视频播放的性能和质量。

所以最终开发分为 Android 端,和 Unity 端,其中 Android 端打包为 aar 给 Unity 端调用。

案例环境:

  • Win 10
  • Unity 2021.3.16f1
  • Android Studio 2021.3.1

Android 端:

1、打开 Android Studio ,新建一个模块

2、编写脚本,添加一个测试视频,模块目录结构如下

3、其他 AndroidManifest.xml 、build.gradle 目前暂时不需要更改,默认就好

4、AndroidVideoPlugin 脚本,主要功能是 把Unity创建的textureId传递过来,绑定到FBO。

然后,注意 MediaPlayer的视频输出,不是SurfaceView了,而是自己建立的SurfaceTextute

5、FBO 文件夹下 是 FBO 处理相关脚本

6、上面搞好后,打包 aar 即可

Unity 端:

1、打开 Unity ,新建一个工程

2、在工程中创建文件夹 Plugin/Android 文件夹,把之前 Android 打的 aar 包导入

3、创建脚本文件夹,编写 AndroidVideoPlugin ,用于获取 Android 端的接口

4、编写一个 Test 脚本,创建 Texture,获取 TextureId 传给 Android 端,

5、构建场景,添加 Quad ,RawImage、几个 Button,以及几个 Text ,最后场景如下

6、把 Test 挂载到 Canvas 上,对应赋值 Quad,RawImage

7、PlayBtn ,QuitBtn ,添加点击事件

8、PlayerSettings 设置中如果Color Space 是 Linear 改为 Gamma , Auto Graphics API 取消勾选

9、取消勾选 Multithreaded Rendering

10、打包运行,效果如上,Android 端的视频渲染到了 Quad 和 RawImage 上

七、关键代码

1、AndroidVideoPlugin.java

java 复制代码
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.util.Log;
import android.view.Surface;

import com.ffalcon.unityshowfromandroidvideomodule.FBO.FBOUtils;
import com.ffalcon.unityshowfromandroidvideomodule.FBO.FilterFBOTexture;

import java.io.IOException;

public class AndroidVideoPlugin implements SurfaceTexture.OnFrameAvailableListener {

    final String TAG = "[AndroidVideoPlugin] ";

    // 引用当前活动的Activity,用于获取资源和播放控制
    private final Activity mActivity;
    // SurfaceTexture用于从相机或视频解码器接收图像帧
    private SurfaceTexture mSurfaceTexture;
    // Surface用于将SurfaceTexture的图像帧呈现到OpenGL ES纹理中
    private Surface mSurface;
    // FilterFBOTexture用于渲染图像帧到FBO并进行过滤处理
    private FilterFBOTexture mFilterFBOTexture;
    // MediaPlayer用于播放视频文件
    private MediaPlayer mMediaPlayer;
    // 标记是否接收到新的图像帧
    private boolean mIsUpdateFrame;

    /**
     * 构造函数,初始化插件并设置Activity
     * @param activity
     */
    public AndroidVideoPlugin(Activity activity) {
        mActivity = activity;
    }

    /**
     * 开始播放视频,将视频帧渲染到指定的Unity纹理ID
     * @param unityTextureId
     * @param width
     * @param height
     */
    public void startPlayVideo(int unityTextureId, int width, int height) {
        Log.i(TAG, "start: unityTextureId=" + unityTextureId);

        // 创建OpenGL ES外部纹理ID
        int videoTextureId = FBOUtils.createOESTextureID();

        // 初始化SurfaceTexture并设置默认缓冲区大小
        mSurfaceTexture = new SurfaceTexture(videoTextureId);
        mSurfaceTexture.setDefaultBufferSize(width, height);
        // 设置帧可用监听器
        mSurfaceTexture.setOnFrameAvailableListener(this);

        // 创建Surface并绑定SurfaceTexture
        mSurface = new Surface(mSurfaceTexture);

        // 初始化FilterFBOTexture,用于渲染和过滤图像帧
        mFilterFBOTexture = new FilterFBOTexture(width, height, unityTextureId, videoTextureId);

        // 初始化MediaPlayer并准备播放
        initMediaPlayer();
    }

    /**
     * 释放资源,包括MediaPlayer、Surface和SurfaceTexture
     */
    public void release() {
        Log.i(TAG, "release: ");

        // 停止并释放MediaPlayer资源
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.reset();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        // 释放Surface资源
        if (mSurface != null) {
            mSurface.release();
            mSurface = null;
        }
        // 释放SurfaceTexture资源
        if (mSurfaceTexture != null) {
            mSurfaceTexture.release();
            mSurfaceTexture = null;
        }
        // 释放FilterFBOTexture资源
        if (mFilterFBOTexture != null) {
            mFilterFBOTexture.release();
            mFilterFBOTexture = null;
        }
        // 重置帧更新标记
        mIsUpdateFrame = false;
    }

    /**
     * 初始化MediaPlayer并设置播放源
     */
    private void initMediaPlayer() {
        // 创建MediaPlayer实例
        mMediaPlayer = new MediaPlayer();
        // 将Surface设置为视频输出
        mMediaPlayer.setSurface(mSurface);
        try {
            // 设置音频流类型
            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            // 设置循环播放
            mMediaPlayer.setLooping(true);

            /**
             * 从文件获取播放源
             */
            /*
            {
                //final File file = new File("/sdcard/test.mp4");
                //mMediaPlayer.setDataSource(Uri.fromFile(file).toString());
            }
            */

            /**
             * 从Assets文件夹获取播放源
             */
            {
                // 从Assets文件夹打开视频文件
                AssetFileDescriptor fd = mActivity.getAssets().openFd("test.mp4");
                // 设置数据源
                mMediaPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
            }

            // 异步准备播放
            mMediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 设置准备完成的监听器
        mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                Log.i(TAG, "onPrepared: ");
                // 播放视频
                mMediaPlayer.start();
            }
        });
    }

    /**
     * 当SurfaceTexture有新帧可用时调用
     * @param surfaceTexture
     */
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        // 标记有新帧到达,需要更新
        mIsUpdateFrame = true;
    }

    // 更新纹理,将新的图像帧渲染到FBO
    public void updateTexture() {
        try {
            // 确保SurfaceTexture不为空
            if(mSurfaceTexture!=null){
                // 标记帧不再需要更新
                mIsUpdateFrame = false;
                // 更新SurfaceTexture中的图像
                mSurfaceTexture.updateTexImage();
                // 绘制图像帧到FBO
                mFilterFBOTexture.draw();
            }
        }catch (IllegalStateException e){
            // 打印异常信息
            Log.i(TAG, "updateTexture: IllegalStateException " +e.toString());
        }
    }

    /**
     * 检查是否有新的图像帧需要更新
     * @return
     */
    public boolean isUpdateFrame() {
        return mIsUpdateFrame;
    }

}

2、FBOUtils.java

java 复制代码
import android.opengl.GLES11Ext;
import android.opengl.GLES30;
import android.util.Log;

public class FBOUtils {

    static String TAG = "[FBOUtils] ";

    /**
     * 编译着色器,根据提供的类型和着色器代码
     * @param type
     * @param shaderCode
     * @return
     */
    public static int compileShader(int type, String shaderCode) {
        // 创建着色器对象
        final int shaderObjectId = GLES30.glCreateShader(type);
        if (shaderObjectId == 0) {
            // 如果创建失败,则返回0
            return 0;
        }
        // 设置着色器的源代码
        GLES30.glShaderSource(shaderObjectId, shaderCode);
        // 编译着色器代码
        GLES30.glCompileShader(shaderObjectId);
        // 检查编译状态
        final int[] compileStatus = new int[1];
        GLES30.glGetShaderiv(shaderObjectId, GLES30.GL_COMPILE_STATUS, compileStatus, 0);
        if (compileStatus[0] == 0) {
            // 如果编译失败,打印错误信息并删除着色器对象
            Log.i(TAG, "compileShader: error --> " + GLES30.glGetShaderInfoLog(shaderObjectId));
            GLES30.glDeleteShader(shaderObjectId);
            return 0;
        }
        return shaderObjectId;
    }

    /**
     * 链接程序,将顶点着色器和片段着色器链接为一个完整的OpenGL程序
     * @param vertexShaderId
     * @param fragmentShaderId
     * @return
     */
    public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
        // 创建OpenGL程序对象
        final int programObjectId = GLES30.glCreateProgram();
        if (programObjectId == 0) {
            // 如果创建失败,则返回0
            return 0;
        }
        // 将顶点着色器附加到程序
        GLES30.glAttachShader(programObjectId, vertexShaderId);
        // 将片段着色器附加到程序
        GLES30.glAttachShader(programObjectId, fragmentShaderId);
        // 链接着色器程序
        GLES30.glLinkProgram(programObjectId);
        // 检查链接状态
        final int[] linkStatus = new int[1];
        GLES30.glGetProgramiv(programObjectId, GLES30.GL_LINK_STATUS, linkStatus, 0);
        if (linkStatus[0] == 0) {
            // 如果链接失败,打印错误信息并删除程序对象
            Log.i(TAG, "linkProgram: error --> " + GLES30.glGetProgramInfoLog(programObjectId));
            GLES30.glDeleteProgram(programObjectId);
            return 0;
        }
        return programObjectId;
    }

    /**
     * 验证程序是否正确链接
     * @param programObjectId
     * @return
     */
    public static boolean validateProgram(int programObjectId) {
        // 验证程序
        GLES30.glValidateProgram(programObjectId);
        // 获取验证状态
        final int[] validateStatus = new int[1];
        GLES30.glGetProgramiv(programObjectId, GLES30.GL_VALIDATE_STATUS, validateStatus, 0);
        return validateStatus[0] != 0;
    }

    /**
     * 构建OpenGL程序,包括编译着色器和链接程序
     * @param vertexShaderSource
     * @param fragmentShaderSource
     * @return
     */
    public static int buildProgram(String vertexShaderSource, String fragmentShaderSource) {
        // 编译顶点着色器
        int vertexShader = compileShader(GLES30.GL_VERTEX_SHADER, vertexShaderSource);
        // 编译片段着色器
        int fragmentShader = compileShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderSource);
        // 链接程序
        int program = linkProgram(vertexShader, fragmentShader);
        // 验证程序
        boolean valid = validateProgram(program);
        Log.i(TAG, "buildProgram: valid = " + valid);
        return program;
    }

    /**
     * 创建帧缓冲对象(FBO)
     * @return
     */
    public static int createFBO() {
        int[] fbo = new int[1];
        GLES30.glGenFramebuffers(fbo.length, fbo, 0);
        return fbo[0];
    }

    /**
     * 创建顶点数组对象(VAO)
     * @return
     */
    public static int createVAO() {
        int[] vao = new int[1];
        GLES30.glGenVertexArrays(vao.length, vao, 0);
        return vao[0];
    }

    /**
     * 创建顶点缓冲对象(VBO)
     * @return
     */
    public static int createVBO() {
        int[] vbo = new int[1];
        GLES30.glGenBuffers(2, vbo, 0);
        return vbo[0];
    }

    /**
     * 创建适用于OpenGL ES外部纹理的纹理ID
     * @return
     */
    public static int createOESTextureID() {
        int[] texture = new int[1];
        GLES30.glGenTextures(texture.length, texture, 0);
        GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
        // 设置纹理参数,优化纹理过滤和边缘处理
        GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
        GLES30.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
        GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE);
        GLES30.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE);
        // 生成纹理的 mipmaps
        GLES30.glGenerateMipmap(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
        return texture[0];
    }

    /**
     * 创建一个2D纹理ID,用于存储渲染到纹理的数据
     * @param width
     * @param height
     * @return
     */
    public static int create2DTextureId(int width, int height) {
        int[] textures = new int[1];
        GLES30.glGenTextures(textures.length, textures, 0);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0]);
        // 初始化纹理图像
        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, width, height, 0,
                GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
        // 设置纹理参数,优化纹理过滤和边缘处理
        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR);
        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);
        // 生成纹理的 mipmaps
        GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
        return textures[0];
    }
}

3、FilterFBOTexture.java

java 复制代码
import android.opengl.GLES11Ext;
import android.opengl.GLES30;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

public class FilterFBOTexture {

    /**
     * 顶点着色器代码,定义了顶点处理的GLSL代码
     */
    private static final String vertexShaderCode =
            "#version 300 es                       \n" +
                    "in vec4 a_Position;           \n" +
                    "in vec2 a_TexCoord;           \n" +
                    "out vec2 v_TexCoord;          \n" +
                    "void main() {                 \n" +
                    "   gl_Position = a_Position;  \n" +
                    "   v_TexCoord = a_TexCoord;   \n" +
                    "}                             \n";

    /**
     * 片段着色器代码,定义了片段处理的GLSL代码,使用外部纹理扩展
     */
    private static final String fragmentShaderCode =
            "#version 300 es                                                 \n" +
                    "#extension GL_OES_EGL_image_external_essl3 : require    \n" +
                    "precision mediump float;                                \n" +
                    "in vec2 v_TexCoord;                                     \n" +
                    "out vec4 fragColor;                                     \n" +
                    "uniform samplerExternalOES s_Texture;                   \n" +
                    "void main() {                                           \n" +
                    "   fragColor = texture(s_Texture, v_TexCoord);          \n" +
                    "}                                                       \n";

    /**
     * 顶点坐标数据,用于定义矩形的四个角
     */
    private final float[] vertexData = {
            -1f, 1f,
            1f, 1f,
            -1f, -1f,
            1f, -1f,
    };
    // 顶点坐标数据的缓冲
    private FloatBuffer vertexBuffer;
    // 顶点缓冲对象(VBO)的ID
    private final int vertexVBO;

    // 纹理坐标数据,用于定义纹理映射到矩形的四个角
    private final float[] textureData = {
            0f, 0f,
            1f, 0f,
            0f, 1f,
            1f, 1f,
    };
    // 纹理坐标数据的缓冲
    private FloatBuffer textureBuffer;
    // 纹理缓冲对象(VBO)的ID
    private final int textureVBO;

    // 着色器程序的ID
    private final int shaderProgram;
    // 顶点着色器中顶点位置属性的索引
    private final int a_Position;
    // 顶点着色器中纹理坐标属性的索引
    private final int a_TexCoord;
    // 片段着色器中纹理采样器的索引
    private final int s_Texture;

    // 纹理的宽度和高度
    private final int width;
    private final int height;
    // Unity端传递过来的纹理ID
    private final int unityTextureId;
    // Android端创建的外部纹理ID
    private final int oesTextureId;
    // 帧缓冲对象(FBO)的ID
    private final int FBO;

    /**
     * 构造函数,初始化资源
     * @param width
     * @param height
     * @param unityTextureId
     * @param oesTextureId
     */
    public FilterFBOTexture(int width, int height, int unityTextureId, int oesTextureId) {
        this.width = width;
        this.height = height;
        this.unityTextureId = unityTextureId;
        this.oesTextureId = oesTextureId;
        // 创建FBO
        FBO = FBOUtils.createFBO();

        // 创建VBO并初始化顶点坐标和纹理坐标的缓冲
        int[] vbo = new int[2];
        GLES30.glGenBuffers(2, vbo, 0);
        vertexBuffer = ByteBuffer.allocateDirect(vertexData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(vertexData);
        vertexBuffer.position(0);
        vertexVBO = vbo[0];

        textureBuffer = ByteBuffer.allocateDirect(textureData.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer()
                .put(textureData);
        textureBuffer.position(0);
        textureVBO = vbo[1];

        // 编译着色器并链接成程序
        shaderProgram = FBOUtils.buildProgram(vertexShaderCode, fragmentShaderCode);
        GLES30.glUseProgram(shaderProgram);
        a_Position = GLES30.glGetAttribLocation(shaderProgram, "a_Position");
        a_TexCoord = GLES30.glGetAttribLocation(shaderProgram, "a_TexCoord");
        s_Texture = GLES30.glGetUniformLocation(shaderProgram, "s_Texture");
    }

    /**
     * 释放资源
     */
    public void release() {
        // 删除纹理
        int[] textures = new int[2];
        textures[0] = oesTextureId;
        textures[1] = unityTextureId;
        GLES30.glDeleteTextures(2, textures, 0);

        // 删除VBO
        int[] vbo = new int[2];
        vbo[0] = vertexVBO;
        vbo[1] = textureVBO;
        GLES30.glDeleteBuffers(2, vbo, 0);
        vertexBuffer.clear();
        textureBuffer.clear();

        // 删除FBO
        int[] fbo = new int[1];
        fbo[0] = FBO;
        GLES30.glDeleteFramebuffers(1, fbo, 0);

        // 删除着色器程序
        GLES30.glDeleteProgram(shaderProgram);
    }

    /**
     * 绘制纹理到FBO
     */
    public void draw() {
        // 设置视口大小
        GLES30.glViewport(0, 0, width, height);

        // 清除帧缓冲
        GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);

        // 绑定FBO
        GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, FBO);
        // 将外部纹理绑定到FBO的颜色附件上
        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, unityTextureId, 0);

        // 使用着色器程序
        GLES30.glUseProgram(shaderProgram);

        // 绑定顶点坐标VBO
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexVBO);
        GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, vertexData.length * 4, vertexBuffer, GLES30.GL_STATIC_DRAW);
        GLES30.glEnableVertexAttribArray(a_Position);
        GLES30.glVertexAttribPointer(a_Position, 2, GLES30.GL_FLOAT, false, 2 * 4, 0);

        // 绑定纹理坐标VBO
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, textureVBO);
        GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, textureData.length * 4, textureBuffer, GLES30.GL_STATIC_DRAW);
        GLES30.glEnableVertexAttribArray(a_TexCoord);
        GLES30.glVertexAttribPointer(a_TexCoord, 2, GLES30.GL_FLOAT, false, 2 * 4, 0);

        // 激活纹理单元并绑定外部纹理
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
        GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);
        GLES30.glUniform1i(s_Texture, 0);

        // 绘制矩形,将纹理映射到矩形上
        GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);

        // 禁用顶点属性数组
        GLES30.glDisableVertexAttribArray(a_Position);
        GLES30.glDisableVertexAttribArray(a_TexCoord);
        // 解绑纹理和VBO
        GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
        GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
    }
}

4、AndroidVideoPlugin.cs

cs 复制代码
using UnityEngine;

/// <summary>
/// AndroidVideoPlugin 实现类
/// </summary>
public class AndroidVideoPlugin : IAndroidVideoPlugin
{
    #region Data

    /// <summary>
    /// TAG
    /// </summary>
    const string TAG = "[AndroidVideoPlugin] ";

    /// <summary>
    /// UnityPlayer CurrentActivity
    /// </summary>
    AndroidJavaObject mCurrentActivity;
    public AndroidJavaObject CurrentActivity
    {
        get { 
            if (mCurrentActivity == null) {
                // 获取当前的 Android Activity
                AndroidJavaObject activity = new AndroidJavaObject("com.unity3d.player.UnityPlayer");
                mCurrentActivity = activity.GetStatic<AndroidJavaObject>("currentActivity");
            } 
            return mCurrentActivity;
        }
    }

    /// <summary>
    /// Android Video Object 
    /// </summary>
    AndroidJavaObject mAndroidVideoObject;

    #endregion

    #region Interface functions

    /// <summary>
    /// 初始化
    /// </summary>
    public void Init() {
        Debug.Log(TAG + "Init():");
        mAndroidVideoObject = new AndroidJavaObject("com.xxxxxx.unityshowfromandroidvideomodule.AndroidVideoPlugin", CurrentActivity);
    }

    /// <summary>
    /// 开始播放视频
    /// </summary>
    /// <param name="textureId"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    public void StartPlayVideo(int textureId, int width, int height) {
        Debug.Log(TAG + $"StartPlayVideo(): textureId = {textureId},  width = {width}, height = {height}");
        mAndroidVideoObject?.Call("startPlayVideo", textureId, width, height);
    }

    /// <summary>
    /// 更新画面
    /// </summary>
    public void UpdateTexture() {
        Debug.Log(TAG + "UpdateTexture(): ");
        if (mAndroidVideoObject.Call<bool>("isUpdateFrame"))
        {
            mAndroidVideoObject?.Call("updateTexture");
            GL.InvalidateState();
        }
    }

    /// <summary>
    /// 释放资源
    /// </summary>
    public void Release() {
        Debug.Log(TAG + "Release(): ");
        if (mAndroidVideoObject != null)
        {
            mAndroidVideoObject?.Call("release");
            mAndroidVideoObject?.Dispose();
            mAndroidVideoObject = null;
        }
    }

    #endregion
}

5、Test.cs

cs 复制代码
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 测试功能类
/// </summary>
public class Test : MonoBehaviour
{
    #region Data
    /// <summary>
    /// 渲染视频的组件
    /// </summary>
    public MeshRenderer MeshRenderer;
    public RawImage RawImage;

    /// <summary>
    /// 设置视口大小
    /// </summary>
    private int mWidth, mHeight;

    /// <summary>
    /// Texture2D
    /// </summary>
    private Texture2D mTexture2D;

    /// <summary>
    /// IAndroidVideoPlugin 
    /// </summary>
    IAndroidVideoPlugin mAndroidVideoPlugin;
    #endregion


    #region Lifecycle functions

    /// <summary>
    /// Use this for initialization
    /// </summary>
    void Start()
    {
        // 根据实际需要设置视口大小
        mWidth = Screen.width;
        mHeight = Screen.height;

        mAndroidVideoPlugin = new AndroidVideoPlugin();
        mAndroidVideoPlugin.Init();
    }

    /// <summary>
    /// Update is called once per frame
    /// </summary>
    void Update()
    {
        if (mTexture2D != null)
        {
            mAndroidVideoPlugin?.UpdateTexture();
        }
    }

    /// <summary>
    /// OnDestroy 
    /// </summary>
    private void OnDestroy()
    {
        if (mTexture2D != null)
        {
            mAndroidVideoPlugin?.Release();

            mTexture2D = null;
        }
    }
    #endregion

    #region public functions

    /// <summary>
    /// 开始播放视频
    /// </summary>
    public void StartPlayVideo()
    {
        if (mTexture2D == null)
        {
            mTexture2D = new Texture2D(mWidth, mHeight, TextureFormat.RGB24, false, false);
            MeshRenderer.material.mainTexture = mTexture2D;
            RawImage.texture = mTexture2D;

            mAndroidVideoPlugin?.StartPlayVideo((int)mTexture2D.GetNativeTexturePtr(),mWidth,mHeight);
        }
    }

    /// <summary>
    /// 退出引用
    /// </summary>
    public void Quit()
    {
        Application.Quit();
    }
    #endregion
}

6、IAndroidVideoPlugin.cs

cs 复制代码
/// <summary>
/// AndroidVideoPlugin 接口类
/// </summary>
public interface IAndroidVideoPlugin 
{
    /// <summary>
    /// 初始化
    /// </summary>
    void Init();

    /// <summary>
    /// 开始播放视频
    /// </summary>
    /// <param name="textureId"></param>
    /// <param name="width"></param>
    /// <param name="height"></param>
    void StartPlayVideo(int textureId, int width, int height);

    /// <summary>
    /// 更新画面
    /// </summary>
    void UpdateTexture();

    /// <summary>
    /// 释放资源
    /// </summary>
    void Release();
}
相关推荐
异次元的归来3 小时前
Unity DOTS中的share component
unity·游戏引擎
拭心3 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
向宇it6 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
带电的小王6 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡6 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道6 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
_oP_i7 小时前
unity webgl部署到iis报错
unity
Go_Accepted7 小时前
Unity全局雾效
unity
向宇it7 小时前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
阿甘知识库7 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站