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外部纹理IDint videoTextureId = FBOUtils.createOESTextureID();// 初始化SurfaceTexture并设置默认缓冲区大小mSurfaceTexture = new SurfaceTexture(videoTextureId);mSurfaceTexture.setDefaultBufferSize(width, height);// 设置帧可用监听器mSurfaceTexture.setOnFrameAvailableListener(this);// 创建Surface并绑定SurfaceTexturemSurface = 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() {@Overridepublic void onPrepared(MediaPlayer mp) {Log.i(TAG, "onPrepared: ");// 播放视频mMediaPlayer.start();}});}/*** 当SurfaceTexture有新帧可用时调用* @param surfaceTexture*/@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {// 标记有新帧到达,需要更新mIsUpdateFrame = true;}// 更新纹理,将新的图像帧渲染到FBOpublic void updateTexture() {try {// 确保SurfaceTexture不为空if(mSurfaceTexture!=null){// 标记帧不再需要更新mIsUpdateFrame = false;// 更新SurfaceTexture中的图像mSurfaceTexture.updateTexImage();// 绘制图像帧到FBOmFilterFBOTexture.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) {// 如果创建失败,则返回0return 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) {// 如果创建失败,则返回0return 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);// 生成纹理的 mipmapsGLES30.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);// 生成纹理的 mipmapsGLES30.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)的IDprivate final int vertexVBO;// 纹理坐标数据,用于定义纹理映射到矩形的四个角private final float[] textureData = {0f, 0f,1f, 0f,0f, 1f,1f, 1f,};// 纹理坐标数据的缓冲private FloatBuffer textureBuffer;// 纹理缓冲对象(VBO)的IDprivate final int textureVBO;// 着色器程序的IDprivate 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端传递过来的纹理IDprivate final int unityTextureId;// Android端创建的外部纹理IDprivate final int oesTextureId;// 帧缓冲对象(FBO)的IDprivate 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;// 创建FBOFBO = 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);// 删除VBOint[] vbo = new int[2];vbo[0] = vertexVBO;vbo[1] = textureVBO;GLES30.glDeleteBuffers(2, vbo, 0);vertexBuffer.clear();textureBuffer.clear();// 删除FBOint[] 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);// 绑定FBOGLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, FBO);// 将外部纹理绑定到FBO的颜色附件上GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, unityTextureId, 0);// 使用着色器程序GLES30.glUseProgram(shaderProgram);// 绑定顶点坐标VBOGLES30.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);// 绑定纹理坐标VBOGLES30.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);// 解绑纹理和VBOGLES30.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 ActivityAndroidJavaObject 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.mzph.cn/pingmian/53745.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

利用熵权法进行数值评分计算——算法过程

1、概述 在软件系统中&#xff0c;研发人员常常遇上需要对系统内的某种行为/模型进行评分的情况。例如根据系统的各种漏洞情况对系统安全性进行评分、根据业务员最近操作系统的情况对业务员工作状态进行打分等等。显然研发人员了解一种或者几种标准评分算法是非常有利于开展研…

word文档无损原样转pdf在windows平台使用python调用win32com使用pip安装pywin32

前提&#xff1a; windows环境下&#xff0c;并且安装了office套装&#xff0c;比如word,如果需要调用excel.也需要安装。在另外的文章会介绍。这种是直接调用word的。所以还原度会比较高。 需求&#xff1a; word文档转pdf,要求使用命令行形式&#xff0c;最终发布为api接口…

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

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

(不用互三)AI绘画工具大比拼:Midjourney VS Stable Diffusion该如何选择?

文章目录 &#x1f4af;如何选择合适的AI绘画工具根据个人需求选择1. 您喜欢什么风格的绘画&#xff1f;2. 您想要创作什么主题的内容&#xff1f;3. 您对绘画工具的使用经验如何&#xff1f; 比较工具特点1. 工具的易用性和功能性如何&#xff1f;易用性&#xff1a;功能性&am…

Qt_自定义信号

目录 1、自定义信号的规定 2、创建自定义信号 3、带参数的信号与槽 4、一个信号连接多个槽 5、信号与槽的断开 结语 前言&#xff1a; 虽然Qt已经内置了大量的信号&#xff0c;并且这些信号能够满足大部分的开发场景&#xff0c;但是Qt仍然允许开发者自定义信号&#…

基于vue框架的宠物寄养系统3d388(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,宠物信息,宠物分类,寄养店,宠物寄养,宠物领养,家庭环境,用户宠物 开题报告内容 基于Vue框架的宠物寄养系统开题报告 一、引言 随着人们生活水平的提高和宠物文化的普及&#xff0c;宠物已成为许多家庭不可或缺的一员。因此&…

实战案例(5)防火墙通过跨三层MAC识别功能控制三层核心下面的终端

如果网关是在核心设备上面&#xff0c;还能用MAC地址进行控制吗&#xff1f; 办公区域的网段都在三层上面&#xff0c;防火墙还能基于MAC来控制吗&#xff1f; 采用正常配置模式的步骤与思路 &#xff08;1&#xff09;配置思路与上面一样 &#xff08;2&#xff09;与上面区…

万象奥科参展“2024 STM32全国巡回研讨会”—深圳站、广州站

9月3日-9月5日&#xff0c;万象奥科参展“2024 STM32全国巡回研讨会”— 深圳站、广州站。此次STM32研讨会将会走进全国11个城市&#xff0c;展示STM32在智能工业、无线连接、边缘人工智能、安全、图形用户界面等领域的产品解决方案及多样化应用实例&#xff0c;深入解读最新的…

Linux系统部署SmartKG(知识图谱安装)

基本要求 #docker需要高版本 Docker version 20.10.14, build a224086docker 20.10.14离线安装 SmartKG官网 官方详细文档 下载部署包 SmartKG官网 准备部署 #上传到服务器 [roottest-server01 opt]# ll SmartKG-master.zip -rw-r--r-- 1 root root 79708691 Sep 11 17:4…

c++基类和派生类对象的赋值转换——赋值兼容规则

1.引出 如下场景&#xff1a; 由于b是double类型&#xff0c;所以赋值给int类型的引用前&#xff0c;要先进行隐式类型转换&#xff0c;这中间会生成临时对象&#xff0c;类是对象具有常性&#xff0c;所以int&之前应该加上const。 但是下面的场景&#xff1a; 没有出现报…

亚马逊测评自建团队与工作室的五大优势亮点,打造高权重评价系统

亚马逊上的产品评价&#xff0c;其实就是为了让买家们说出他们的真实想法&#xff0c;这样卖家就能知道怎么把东西做得更好&#xff0c;让买家更满意&#xff0c;还能让卖东西的招数更给力。效果有以下几点&#xff1a; 1. 商品更靠谱&#xff1a;买家说好&#xff0c;大家就更…

基于SSM的校园志愿者管理系统的设计与实现---附源码76245

摘 要 本文基于SSM框架&#xff0c;设计并实现了一套校园志愿者管理系统&#xff0c;旨在提高校园志愿服务管理的效率和质量。系统主要包括管理员、志愿者和活动发布者三大角色&#xff0c;涵盖了志愿者管理、活动管理、公告管理等功能模块&#xff0c;采用了MySQL作为数据库&…

【计算机组成原理】详细解读带符号整数在计算机中的运算

有符号整数的运算 导读一、补码的优势二、补码的加法运算三、补码的减法运算四、原码、反码、补码的特性结语 导读 大家好&#xff0c;很高兴又和大家见面啦&#xff01;&#xff01;&#xff01; 经过前面的介绍&#xff0c;我们已经初步认识了有符号整数的三种表示形式&…

NPU 与 GPU 相比,有什么差别?| 技术速览

编者按&#xff1a; 随着2024年被业界誉为“AI PC元年”&#xff0c;各大笔记本电脑厂商纷纷推出搭载NPU的全新AI PC&#xff0c;而在介绍产品性能时&#xff0c;“NPU”一词频频被提及。但NPU和我们所熟知的GPU之间的区别究竟是什么&#xff1f; 我们今天为大家分享的这篇文章…

电水壶自复位热断循环测试合规性

在家用电器安全标准中,电水壶的安全性尤为重要,尤其是涉及热保护装置的部分。电水壶在日常使用中频繁接触高温水,极端情况下,温度可能异常升高。因此,为了确保用户的安全,热保护装置必须可靠工作。本文将探讨自复位热断路器(TCO)在电水壶中的作用,以及在100次循环测试…

如何在 Selenium 中获取网络调用请求?

引言 捕获网络请求对于理解网站的工作方式以及传输的数据至关重要。Selenium 作为一种 Web 自动化工具,可以用于捕获网络请求。本文将讨论如何使用 Selenium 在 Java 中捕获网络请求并从网站检索数据。 我们可以使用浏览器开发者工具轻松捕获网络请求或日志。大多数现代 Web…

creating chat agent with langchain and openai getting no attribute error

题意&#xff1a; 使用 LangChain 和 OpenAI 创建聊天代理时遇到“没有属性错误”&#xff08;Getting "no attribute" error when creating a chat agent with LangChain and OpenAI&#xff09; 问题背景&#xff1a; Im trying to test a chat agent using the …

房产销售系统|基于java和vue的房产销售系统(源码+数据库+文档)

房产销售|房地产|卖房系统 目录 基于java和vue的房产销售系统 一、前言 二、系统设计 三、系统功能设计 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码农|毕设布道师&#xff0c;…

95分App全程正品保障,赋能闲置消费新风尚

在当今快节奏、高消费的时代&#xff0c;闲置经济正以前所未有的速度崛起&#xff0c;成为新一代消费者的新宠。越来越多的年轻人开始拥抱闲置商品&#xff0c;将“断舍离”与“物尽其用”的理念融入日常生活&#xff0c;催生了闲置交易市场的空前繁荣。曾几何时&#xff0c;购…

【iOS】UIViewController的生命周期

UIViewController的生命周期 文章目录 UIViewController的生命周期前言UIViewController的一个结构UIViewController的函数的执行顺序运行代码viewWillAppear && viewDidAppear多个视图控制器跳转时的生命周期pushpresent 小结 前言 之前对于有关于UIViewControlller的…