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

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

目录

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

一、简单介绍

二、FBO 简单介绍

三、案例实现原理

四、注意事项

五、简单效果预览

六、案例实现步骤

Android 端:

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为例子,一个绑定代码如下:

// 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


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


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


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

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


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


/// <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();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/875376.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

03 Flask-添加配置信息

回顾之前学习的内容 02 Flask-快速上手 Flask 中最简单的web应用组成 1. 导入核心库 Flask from flask import Flask2. 实例化 web应用 注意&#xff1a;不要漏了 app Flask(__name__) 中的 __name__ 表示&#xff1a;是从当前的py文件实例化 app Flask(__name__)3. 创…

力扣每日一题:1372.二叉树中的最长交错路径

题目 给你一棵以 root 为根的二叉树&#xff0c;二叉树中的交错路径定义如下&#xff1a; 选择二叉树中 任意 节点和一个方向&#xff08;左或者右&#xff09;。如果前进方向为右&#xff0c;那么移动到当前节点的的右子节点&#xff0c;否则移动到它的左子节点。改变前进方…

力扣213-打家劫舍 II(Java详细题解)

题目链接&#xff1a;213. 打家劫舍 II - 力扣&#xff08;LeetCode&#xff09; 前情提要&#xff1a; 本体是打家劫舍的一个变形题&#xff0c;希望大家能先做198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09;&#xff0c;并看一下我上题的讲解力扣198-打家劫舍&…

制证书、制电子印章、签章 -- 演示程序说明

ofd签章系统涉及证书的制作、电子印章制作、签章、验章等环节。关于ofd签章原理&#xff0c;本人写过多篇文章进行了阐述; 见文章《ofd板式文件 电子签章实现方法》、《一款简单易用的印章设计工具》、《签章那些事 -- 让你全面了解签章的流程》。 为了进一步加深对签章过程的理…

RK3229 ADNROID9 hdmi与耳机口同出声音

声卡0怎么配置才能跟HDMI同时输出一样的声音&#xff0c;下面是具体描述&#xff1a; 1、硬件连接 声卡0的连接是芯片的ADC音频输出脚直接接到DA芯片输出 2、cat /proc/asound/cards 0 [rockchiprk3229 ]: rockchip_rk3229 - rockchip,rk3229 rockchip,rk3229 1 [rockchiphdmi …

MFC工控项目实例之十一板卡测试信号输入界面

承接专栏《MFC工控项目实例之十添加系统测试对话框》 相关代码 1、在BoardTest.h文件中添加代码 class CBoardTest : public CDialog { // Construction public:CBoardTest(CWnd* pParent NULL); // standard constructorCButtonST m_btnStart[16];CWinThread* pThread…

FAT32文件系统详细分析 (格式化SD nandSD卡)

FAT32 文件系统详细分析 (格式化 SD nand/SD 卡) 目录 FAT32 文件系统详细分析 (格式化 SD nand/SD 卡)1. 前言2.格式化 SD nand/SD 卡3.FAT32 文件系统分析3.1 保留区分析3.1.1 BPB(BIOS Parameter Block) 及 BS 区分析3.1.2 FSInfo 结构扇区分析3.1.3 引导扇区剩余扇区3.1.4 …

RocketMQ 基础入门

文章内容是学习过程中的知识总结&#xff0c;如有纰漏&#xff0c;欢迎指正 文章目录 前言 RocketMQ 特点 RocketMQ 优势 1. RocketMQ 基本概念 1.1 NameServer 1.1.1 NameServer作用 1.1.2 和zk的区别 1.1.3 高可用保障 1.2 Broker 1.2.1 部署方式 1.2.1.1 单 Master 1.2.1.2 …

C语言 | Leetcode C语言题解之第396题旋转函数

题目&#xff1a; 题解&#xff1a; #define MAX(a, b) ((a) > (b) ? (a) : (b))int maxRotateFunction(int* nums, int numsSize){int f 0, numSum 0;for (int i 0; i < numsSize; i) {f i * nums[i];numSum nums[i];}int res f;for (int i numsSize - 1; i &g…

多维时序 | Matlab基于SSA-SVR麻雀算法优化支持向量机的数据多变量时间序列预测

多维时序 | Matlab基于SSA-SVR麻雀算法优化支持向量机的数据多变量时间序列预测 目录 多维时序 | Matlab基于SSA-SVR麻雀算法优化支持向量机的数据多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab基于SSA-SVR麻雀算法优化支持向量机的数据多变…

Docker部署tenine实现后端应用的高可用与负载均衡

采用Docker方式的Tengine 和 keepalived 组合模式可以实现小应用场景的高可用负载均衡需求 目录 网络架构一、环境准备二、软件安装1. 下载Tenine镜像2. 下载Keepalived镜像3. 制作SpringBoot镜像 三、软件配置1. 创建应用容器2. 代理访问应用3. 创建Keepalived4. 测试高可用 网…

CSP-J算法基础 树状结构与二叉树

文章目录 前言树状结构树状结构的基本概念&#xff1a;为什么需要树状结构&#xff1f;优点树状结构的示例 二叉树什么是二叉树&#xff1f;二叉树的类型什么样的树不是二叉树&#xff1f;二叉树的五种形态 完全二叉树相关概念完全二叉树的定义&#xff1a; 相关概念1. **高度&…

Xcode报错:No exact matches in reference to static method ‘buildExpression‘

Xcode报错1&#xff1a;No exact matches in reference to static method buildExpression Xcode报错2&#xff1a;Type () cannot conform to View 这两个报错都是因为在SwiftUI的View的Body里面使用了ForEach循环,却没有在ForEach循环闭包的内部返回视图&#xff0c;而是做了…

数据库安全性控制

‍ 在当今信息化时代&#xff0c;数据库安全性 对于保护数据免受非法访问和损害至关重要。无论是个人数据还是企业机密&#xff0c;数据库安全性控制都能有效地防范潜在的威胁。本文将为你深入浅出地介绍数据库安全性控制的关键方法和机制&#xff0c;帮助你轻松掌握这一重要概…

数据库基础知识---------------------------(1)

数据库分类 关系型数据库 以表格方式存储数据 例子&#xff1a; MySQL、Oracle、DB2、SQLserver等 特点&#xff1a; SQL结构程度较高、安全性高、查询效率较低 非关系型数据库 以键值方式存储数据 例子&#xff1a; Redis、Hbase、MongoDB等 特点&#xff1a; 查询效率…

深度学习的零碎知识点

显卡内存 什么是显卡内存 简单来说就是&#xff0c;Windows 会在物理显存/「专用 GPU 内存」不够用或只有集成显卡的情况下&#xff0c;将物理内存 RAM 当作 GPU 的虚拟显存/「共享 GPU 内存」来使用。 什么是 Windows「共享 GPU 内存」&#xff0c;它与 VRAM 有什么不同 (s…

C# 使用Socket通信,新建WinForm服务端、客户端程序

一、新建WinForm Socket服务端程序 注&#xff1a;rtbReceviceMsg为RichTextBox控件 服务端程序、界面 服务端代码 public partial class Form1 : Form {public Form1(){InitializeComponent();}public virtual void TriggerOnUpdateUI(string message){if (this.InvokeRequir…

软件测试学习笔记丨Pytest的使用

本文转自测试人社区&#xff0c;原文链接&#xff1a;https://ceshiren.com/t/topic/22158 1. 简介 pytest是一个成熟的全功能python测试框架测试用例的skip和xfail&#xff0c;自动失败重试等处理能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium/ap…

Hive任务优化参数整理

Hive本身是个基于hdfs的结构化数据管理工具&#xff0c;虽然在后面的发展中允许底层接入其他的数据源&#xff0c;比如第三方数据服务这种基础架构&#xff0c;但是它从立意上来说&#xff0c;它不适合用来做高性能查询引擎&#xff0c;反而在传统离线数据仓库中它有着自身的优…

python 函数 封装

封装 函数的参数是&#xff1a;变量 def 函数(参数):print(参数)if __name__ __main__:函数(参数)函数(参数2)函数的参数是&#xff1a; 字典 import requests# 定义一个字典 data {} 地址 "https://webdriveruniversity.com/" 请求方法 getdata["url"…