安卓下多线程OpenGL共享Context (四)

之前的方案假定Java层更新纹理时使用的是RGB或RBGA格式的数据,但是在播放视频这种应用场景下,解码器解码出来的数据如果是YUV格式,渲染起来就比较麻烦了。一种方式是使用CPU进行YUV转RGB,然后再进行渲染,但是这种方式性能极差;另一种方式是使用GPU进行转换,利用GPU的并行计算能力加速转换。我们需要编写Shader来实现。如前文所述,Unity只需要Java层的纹理ID,当使用Shader进行YUV转RGB时,怎么实现更新该纹理的数据呢?答案是Render to Texture (参见[1])。具体做法是,创建一个FrameBuffer,调用glFramebufferTexture2D将纹理与FrameBuffer关联起来,这样在FrameBuffer上进行的绘制,就会被写入到该纹理中。Java代码如下:

public void setupGL(int width, int height) {
    // 创建纹理
    int tempBuffer[] = new int[1];
    GLES20.glGenTextures(1, tempBuffer, 0);
    mTextureId = tempBuffer[0];
    if (mTextureId == 0) {
        glLogE("setupGL, glGenTextures for render texture failed");
        return;
    }
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    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);
    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA,
            GLES20.GL_UNSIGNED_BYTE, null);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

    // 创建YUV纹理
    GLES20.glGenTextures(3, mYuvTextures, 0);
    if (mYuvTextures[0] == 0 || mYuvTextures[1] == 0 || mYuvTextures[2] == 0) {
        MyLog.e(TAG, "setupGL, glGenTextures for yuv texture failed");
        return;
    }
    for (int yuvTexture : mYuvTextures) {
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTexture);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        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);
    }
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

    // 创建帧缓冲区
    GLES20.glGenFramebuffers(1, tempBuffer, 0);
    mFramebuffer = tempBuffer[0];
    if (mFramebuffer == 0) {
        glLogE("setupGL, glGenFramebuffers failed");
        return;
    }
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer);
    GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
            GLES20.GL_TEXTURE_2D, mTextureId, 0);
    int errCode = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
    if (errCode != GLES20.GL_FRAMEBUFFER_COMPLETE) {
        glLogE("setupGL, glCheckFramebufferStatus failed, errCode=0x" + Integer.toHexString(errCode));
        return;
    }
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}

绘制时,先绑定FrameBuffer,再进行绘制操作,Java代码如下:

public void updateTexture() {
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer);
    GLES20.glViewport(0, 0, mWidth, mHeight);
    GLES20.glClearColor(0, 0, 0, 1);
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    // 此处添加绘制操作
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}

为了实现YUV数据的渲染,需要编写Shader。此处,我们渲染的YUV数据格式为YUV420 (YUV420格式的具体介绍,请读者自行百度),Y、U、V通道数据分别存放在三个缓冲区中。将YUV数据分别赋给三个纹理,然后指定Shader的顶点坐标和纹理坐标,绘制一个矩形即可 (参见[2])。

首先,在setupGL函数中为YUV生成三个纹理,Java代码如下:

// 创建YUV纹理
GLES20.glGenTextures(3, mYuvTextures, 0);
if (mYuvTextures[0] == 0 || mYuvTextures[1] == 0 || mYuvTextures[2] == 0) {
    MyLog.e(TAG, "setupGL, glGenTextures for yuv texture failed");
    return;
}
for (int yuvTexture : mYuvTextures) {
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTexture);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    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);
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

绘制时,将YUV数据分别赋给三个纹理,并将三个纹理分别与GLES20.GL_TEXTURE0,GLES20.GL_TEXTURE1,GLES20.GL_TEXTURE2绑定,Java代码如下:

for (int i = 0; i < 3; ++i) {
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mYuvTextures[i]);
    int w = i == 0 ? yuvFrame.yuvStrides[0] : (yuvFrame.yuvStrides[0] / 2);
    int h = i == 0 ? yuvFrame.height : (yuvFrame.height / 2);
    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0,
            GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, yuvFrame.yuvPlanes[i]);
}

  然后,我们需要创建Program,为Program创建Vertex Shader和Fragment Shader (参见[2])。Java代码如下:

public static final String VERTEX_SHADER_STRING = "attribute vec4 in_pos;\n"
        + "attribute vec2 in_tc;\n"
        + "varying vec2 out_tc;\n"
        + "void main() {\n"
        + "    gl_Position = in_pos;\n"
        + "    out_tc = in_tc;\n"
        + "}\n";

public static final String FRAGMENT_SHADER_STRING = "precision mediump float;\n"
        + "uniform sampler2D tex_y;\n"
        + "uniform sampler2D tex_u;\n"
        + "uniform sampler2D tex_v;\n"
        + "varying vec2 out_tc;\n"
        + "void main() {\n"
        + "    vec4 c = vec4((texture2D(tex_y, out_tc).r - 16./255.) * 1.164);\n"
        + "    vec4 U = vec4(texture2D(tex_u, out_tc).r - 128./255.);\n"
        + "    vec4 V = vec4(texture2D(tex_v, out_tc).r - 128./255.);\n"
        + "    c += V * vec4(1.596, -0.813, 0, 0);\n"
        + "    c += U * vec4(0, -0.392, 2.017, 0);\n"
        + "    c.a = 1.0;\n"
        + "    gl_FragColor = c;\n"
        + "}\n";

protected final void addShaderTo(int type, String source, int program) throws RuntimeException {
    int shader = GLES20.glCreateShader(type);
    if (shader == 0) {
        throw new RuntimeException("Create shader failed, err=" + GLES10.glGetError());
    }
    GLES20.glShaderSource(shader, source);
    GLES20.glCompileShader(shader);
    int[] result = new int[]{GLES20.GL_FALSE};
    GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, 0);
    if (result[0] != GLES20.GL_TRUE) {
        GLES20.glDeleteShader(shader);
        throw new RuntimeException("Compile shader failed, err=" + GLES10.glGetError());
    }
    GLES20.glAttachShader(program, shader);
    GLES20.glDeleteShader(shader);
}

public void setupGL(int width, int height) {
    ...
    // 创建Program
    mProgram = GLES20.glCreateProgram();
    addShaderTo(GLES20.GL_VERTEX_SHADER, EglRender.VERTEX_SHADER_STRING, mProgram);
    addShaderTo(GLES20.GL_FRAGMENT_SHADER, EglRender.FRAGMENT_SHADER_STRING, mProgram);
    GLES20.glLinkProgram(mProgram);
    GLES20.glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, tempBuffer, 0);
    if (tempBuffer[0] != GLES20.GL_TRUE) {
        glLogE("setupGL, create program failed");
        return;
    }
    GLES20.glUseProgram(mProgram);
    int y_tex = GLES20.glGetUniformLocation(mProgram, "tex_y");
    GLES20.glUniform1i(y_tex, 0);
    int u_tex = GLES20.glGetUniformLocation(mProgram, "tex_u");
    GLES20.glUniform1i(u_tex, 1);
    int v_tex = GLES20.glGetUniformLocation(mProgram, "tex_v");
    GLES20.glUniform1i(v_tex, 2);
    mVertexLocation = GLES20.glGetAttribLocation(mProgram, "in_pos");
    mTextureLocation = GLES20.glGetAttribLocation(mProgram, "in_tc");
    GLES20.glUseProgram(0);
}

完整绘制代码如下:

 1 public void updateTexture() {
 2     GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebuffer);
 3
 4     GLES20.glViewport(0, 0, mWidth, mHeight);
 5     GLES20.glClearColor(0, 0, 0, 1);
 6     GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
 7     GLES20.glDisable(GLES20.GL_CULL_FACE);
 8     GLES20.glDisable(GLES20.GL_DEPTH_TEST);
 9     GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
10     GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);
11
12     GLES20.glUseProgram(mProgram);
13     // 更新YUV数据
14     for (int i = 0; i < 3; ++i) {
15         GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
16         GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mYuvTextures[i]);
17         int w = i == 0 ? yuvFrame.yuvStrides[0] : (yuvFrame.yuvStrides[0] / 2);
18         int h = i == 0 ? yuvFrame.height : (yuvFrame.height / 2);
19         GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE, w, h, 0,
20                 GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE, yuvFrame.yuvPlanes[i]);
21     }
22     // 设置顶点坐标和纹理坐标
23     GLES20.glEnableVertexAttribArray(mVertexLocation);
24     mVertexCoord.position(0);
25     GLES20.glVertexAttribPointer(mVertexLocation, 2, GLES20.GL_FLOAT, false, 0, mVertexCoord);
26     GLES20.glEnableVertexAttribArray(mTextureLocation);
27     mTextureCoord.position(0);
28     GLES20.glVertexAttribPointer(mTextureLocation, 2, GLES20.GL_FLOAT, false, 0, mTextureCoord);
29     // 绘制矩形
30     GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
31
32     GLES20.glDisableVertexAttribArray(mVertexLocation);
33     GLES20.glDisableVertexAttribArray(mTextureLocation);
34     for (int i = 0; i < 3; ++i) {
35         GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
36         GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
37     }
38
39     GLES20.glUseProgram(0);
40     GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
41     GLES20.glClear(0);
42 }

其中,9~10行是必需的,否则,glDrawArrays调用会失败,报GL_INVALID_OPERATION错误 (参见[3])。当绘制完成之后,需要通知Unity3D,在C#中调用GL.InvalidateState,否则会影响Unity的绘制 (参见[4])。C#代码如下:

void Update () {
    mPluginTexture.Call ("updateTexture");
    GL.InvalidateState ();
}

其中,Update函数是Unity在每次刷新帧时回调,我们在该回调中调用Java层的updateTexture函数更新纹理数据,然后调用GL.InvalidateState,通知Unity重置OpenGL状态。笔者一开始采用当Java解码出一帧时,便更新纹理数据进行绘制,然后通知C#调用GL.InvalidateState。但是这种方式存在两个问题,一是当App退到后台,绘制操作仍会进行,只是绘制会失败;二是画面更新一段时间之后,便会卡住,过很久才会恢复。具体原因并未查出。后来,改用从C#的Update回调更新纹理,这两个问题得以解决。

总结:

Unity官方给出的Plugin绘制方式是通过在C#层调用GL.IssuePluginEvent,C++层接收从Unity Render线程过来的回调,在该回调中更新纹理 (参见[5])。该方案需要编写JNI和C++,实现起来比较麻烦。本文给出的方案全部在Java层即可实现。供感兴趣的读者参考。

[参考文献]

[1] Tutorial 14 : Render To Texture

[2] 最简单的视音频播放示例6:OpenGL播放YUV420P(通过Texture,使用Shader)

[3] robertcastle/UnityFBO

[4] GL.InvalidateState

[5] Low-level Native Plugin Interface

时间: 2024-08-11 09:26:17

安卓下多线程OpenGL共享Context (四)的相关文章

安卓下多线程OpenGL共享Context (二)

为了在Java线程进行OpenGL调用,需要为java线程初始化OpenGL环境,initOpenGL函数展示了初始化OpenGL环境的过程.在setupOpenGL方法中,在线程上先执行该调用即可.Java代码示例如下: 1 package com.thornbirds.unity; 2 3 public class PluginTexture { 4 5 private EGLDisplay mEGLDisplay; 6 private EGLConfig mEglConfig; 7 pri

安卓下多线程OpenGL共享Context (三)

在上一节中我们介绍了多线程OpenGL绘制方案,但是如果需要在Java线程不断修改纹理数据,会由于并发访问导致Unity线程出现访问非法内存而崩溃.因此,考虑在Java线程加载数据,然后在unity线程调用OpenGL操作更新纹理.这样所有的OpenGL操作都在Unity绘制线程完成,从而避免了多线程OpenGL引入的各种问题.为了能够从Java线程切换到Unity线程执行,我们获取到Unity线程的Looper,然后使用该Looper实例化一个Handler,这样就可以通过往上发送消息或者Ru

安卓下多线程OpenGL共享Context (一)

最近在研究Unity3D开发中使用Java Plugin进行纹理更新,想法很简单,在Java线程更新纹理数据,然后Unity场景中的纹理将自动更新. 首先,创建Java类,定义创建纹理及获取纹理参数的接口,并创建单线程池用于进行加载Bitmap并绑定纹理数据等OpenGL操作.Java代码示例: 1 package com.thornbirds.unity; 2 3 import android.graphics.Bitmap; 4 import android.graphics.BitmapF

线程同步-iOS多线程编程指南(四)-08-多线程

首页 编程指南 Grand Central Dispatch 基本概念 多核心的性能 Dispatch Sources 完结 外传:dispatch_once(上) Block非官方编程指南 基础 内存管理 揭开神秘面纱(上) 揭开神秘面纱(下) iOS多线程编程指南 关于多线程编程 线程管理 Run Loop 线程同步 附录 Core Animation编程指南 Core Animation简介 基本概念 渲染架构 几何变换 查看目录 中文手册/API ASIHTTPRequest Openg

安卓笔记2——安卓下的测试和数据存储方式

今天开始介绍安卓的另一个基础知识,安卓下的测试和数据存储的几种方式. 以后后同步发出对应笔记.老规矩,用一张图来介绍今天的内容. 图片看不清的话可以右键新窗口打开. 一.测试 1,分类 黑盒测试: 是以用户的角度,从输入数据与输出数据的对应关系出发进行测试的. 白盒测试: 又称结构测试.透明盒测试.逻辑驱动测试或基于代码的测试. 单元测试: 又称模块测试,是开发者编写的一小段代码,用于检验被测代码的一个很小的.很明确的功能是否正确. 功能测试: 根据产品特性.操作描述和用户方案,测试一个产品的特

linux下多线程编程

最近研究mysql源码,各种锁,各种互斥,好在我去年认真学了<unix环境高级编程>, 虽然已经忘得差不多了,但是学过始终是学过,拿起来也快.写这篇文章的目的就是总结linux 下多线程编程,作为日后的参考资料. 本文将介绍linux系统下多线程编程中,线程同步的各种方法.包括: 互斥量(mutex) 读写锁 条件变量 信号量 文件互斥 在介绍不同的线程同步的方法之前,先简单的介绍一下进程和线程的概念, 它们的优缺点,线程相关的API,读者——写者问题和哲学家就餐问题. 基础知识 1. 进程和

用redis实现tomcat集群下的session共享

上篇实现了 LINUX中NGINX反向代理下的TOMCAT集群(http://www.cnblogs.com/yuanjava/p/6850764.html) 这次我们在上篇的基础上实现session 共享问题 Nginx机器:192.168.1.108 两台tomcat机器分别是:192.168.1.168 192.168.1.178 一:测试session共享问题 在原index.jsp页面添加如下代码 SessionId:<%= session.getId() %> <% Stri

带你玩转java多线程系列 “道篇” 多线程的优势及利用util.concurrent包测试单核多核下多线程的效率

java多线程 “道篇” - 多线程的优势及用concurrent包测试单核多核下多线程的效率 1 超哥对于多线程自己的理解 2 测试代码 3 CountDownLatch这个同步辅助类科普 4 如何把电脑设置成单核 5 测试结果 1 超哥对于多线程自己的理解 超哥的理解:对于多线程,无非是对于顺序执行下任务的一种抽取和封装,将原来顺序执行的任务单独拿出来放到线程类的run方法中,通过线程类的start方法进行执行,对于多线程访问共同资源时,我们需要加锁,也就是只有某个线程在拥有锁的时候,才能够

【OGG】RAC环境下配置OGG单向同步 (四)

[OGG]RAC环境下配置OGG单向同步 (四) 一.1  BLOG文档结构图 一.2  前言部分 一.2.1  导读 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不知道的知识,~O(∩_∩)O~: ① RAC环境下配置OGG单向同步 注意:本篇BLOG中代码部分需要特别关注的地方我都用黄色背景和红色字体来表示,比如下边的例子中,thread 1的最大归档日志号为33,thread 2的最大归档日志号为43是需要特别关注的地方. List of Archived Lo