前言

前面学习了opengl es的基础知识,包括GLSL语言,常用函数等等,由于opengl es是基于夸平台的api设计,它本身不提供上下文管理,窗口管理,这些交给具体的设备厂商。在安卓平台这些是由EGL库实现的,接下来我们就学习安卓平台如何搭建opengl es的环境;安卓平台的EGL库分为java层,在com.media.opengl_es包下;native层的EGL库则需要引入头文件

#include <EGL/egl.h>
#include <EGL/eglext.h>

opengl es系列文章

opengl es之-基础概念(一)
opengl es之-GLSL语言(二)
opengl es之-GLSL语言(三)
opengl es之-常用函数介绍(四)
opengl es之-安卓平台在图片上画对角线和截屏(十一)

目标

1、GLSurfaceView搭建opengl es环境
2、SurfaceView搭建opengl es环境
3、TextureView搭建opengl es环境
4、利用上面搭建的环境渲染一张图片到屏幕上

GLSurfaceView、SurfaceView、TextureView区别

1、SurfaceView,它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface(所有的普通View是共享一个Surface的,该Surface由他们的共同父类DecorView提供),利用这个Surface,我们可以进行单独的渲染,并将最终的渲染结果与DecorView合并最终输出到屏幕上。流程如下:

 

1561205389814.jpg

这里的Surface其实就是前面章节所讲的frame buffer和绑定了frame buffer的 render buffer的封装体,所以在安卓平台利用opengl es并不需要我们再去通过
glGenframebuffers()和glGenRenderbuffers()函数创建FBO和RBO了,因为SurfaceView已经为我们封装好了。
2、GLSurfaceView集成于SurfaceView,它具有SurfaceView的全部特性,内部实现了EGL管理和一个独立的渲染线程,而SurfaceView却需要我们自己实现EGL和渲染线程
3、TextureView继承于View,它内部没有Surface,但是有一个SurfaceTexture,该SurfaceTexture可以作为EGL的参数来创建Surface,SurfaceTexture就相当于frame buffer。使用流程和SurfaceView一致。

GLSurfaceView搭建Opengl es环境

通过前面的学习,我们可以知道,使用GLSurfaceView搭建opengl es环境是最简单的,因为不需要我们自己实现EGL,和对渲染线程的管理了,下面来看看如何使用GLSurfaceView
1、像创建普通View一样创建GLSurfaceView

int h = PixelUtil.dp2px(this,200);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,h);
lp.addRule(RelativeLayout.BELOW, R.id.id_btn3);
lp.topMargin = PixelUtil.dp2px(this,20);
lp.leftMargin = PixelUtil.dp2px(this,20);
lp.rightMargin = PixelUtil.dp2px(this,20);

glSurfaceView = new MyGLSurfaceView(this);
contentLayout.addView(glSurfaceView);
glSurfaceView.setLayoutParams(lp);

MyGLSurfaceView是GLSurfaceView的子类

public class MyGLSurfaceView extends GLSurfaceView {

    // 先保存要显示的纹理
    private Bitmap mBitmap;
    private int mWidth;
    private int mHeight;
    // 顶点坐标
    private ByteBuffer vbuffer;
    // 纹理坐标
    private ByteBuffer fbuffer;

    // 1、初始化GLSurfaceView,包括调用setRenderer()设置GLSurfaceView.Renderer对象
    // setRenderer()将会创建一个渲染线程
    public MyGLSurfaceView(Context context) {
        super(context);
        initGLESContext();
    }

    public MyGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initGLESContext();
    }

    /** 遇到问题:Opengl es函数无法工作,由于没有指定opengl es的版本
     *  解决方案:设置正确的opengl es版本
     **/
    private void initGLESContext() {
        // 设置版本,必须要
        setEGLContextClientVersion(2);

        GLRGBRender render = new GLRGBRender();
        setRenderer(render);
        setRenderMode(RENDERMODE_WHEN_DIRTY); // 默认是连续渲染模式
    }
......
}

注意initGLESContext()函数
要调用setEGLContextClientVersion(2);设置opengl es的版本
调用setRenderer(render);指定GLSurfaceView.Renderer接口的实现者,点进去可以看到此函数调用后GLSurfaceView内部的渲染线程GLThread将自动启用

public void setRenderer(Renderer renderer) {
        checkRenderThreadState();
        if (mEGLConfigChooser == null) {
            mEGLConfigChooser = new SimpleEGLConfigChooser(true);
        }
        if (mEGLContextFactory == null) {
            mEGLContextFactory = new DefaultContextFactory();
        }
        if (mEGLWindowSurfaceFactory == null) {
            mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
        }
        mRenderer = renderer;
        mGLThread = new GLThread(mThisWeakRef);
        mGLThread.start();
    }

那么有人可能会想,还没开始绘制,渲染线程就空跑起来了?那岂不是很浪费啊,对的,但是我们可以通过如下函数指定该线程的行为
setRenderMode(RENDERMODE_WHEN_DIRTY); // 默认是连续渲染模式
RENDERMODE_CONTINUOUSLY:
默认情况下,渲染线程每隔16ms自动发出一次渲染请求。
RENDERMODE_WHEN_DIRTY:
则表示渲染请求由用户调用requestRender();函数后触发;备注:GLSurfaceView创建成果后也会触发几次渲染请求
2、实现GLSurfaceView.Renderer接口
前面提到了setRenderer(render)函数,这里的render就是实现了GLSurfaceView.Renderer接口的类,该接口告诉了我们EGL环境、Surface的准备情况,我们的opengl es绘制指令要在该接口中去实现,先看下该接口的介绍
public interface Renderer {
....
void onSurfaceCreated(GL10 gl, EGLConfig config);
void onSurfaceChanged(GL10 gl, int width, int height);
void onDrawFrame(GL10 gl);
....
}
void onSurfaceCreated(GL10 gl, EGLConfig config):
代表GLSurfaceView内部的Surface已经创建好,EGL环境也准备就绪,同时我们可以在该回调中重新配置EGLConfig,回调结束后将使用新的EGLConfig配置的EGL环境,当然也可以不做处理使用默认配置。

void onSurfaceChanged(GL10 gl, int width, int height);
当GLSurfaceView的大小改变时往往会伴随着内部Surface大小的改变,此时该回调会被调用,GLSurfaceView首次初始化时该函数也会被执行

void onDrawFrame(GL10 gl);
如果前面是默认的RENDERMODE_WHEN_DIRTY渲染模式,那么此函数每隔16ms执行一次;如果是RENDERMODE_WHEN_DIRTY渲染模式,那么此函数
在GLSurfaceView的requestRender();函数调用后被调用
opengl es函数指令应该在该函数中去执行

@Override
public void onDrawFrame(GL10 gl) {
   MLog.log("onDrawFrame thread " + Thread.currentThread()); 
    ........
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);            
}

这里只是讲解opengl es的环境搭建,就不贴出具体的opengl es的代码了,具体可以参考Demo中的MyGLSurfaceView类
备注:它是在渲染线程中被调用

3、GLSurfaceView释放
使用完毕后还要对GLSurfaceView进行释放,那么释放就跟随Activity的生命周期了

至此,GLSurfaceView环境的搭建就讲解完毕了

SurfaceView搭建opengl es环境

SurfaceView与GLSurfaceView一样,它内部会自动创建一个Surface作为opengl es的渲染缓冲区。区别就是不提供EGL的实现和渲染线程的管理,所以要使用SurfaceView搭建Opengl es环境我们得自己实现EGL和渲染线程。

1、实现EGL环境
整个EGL环境的实现有如下几个很重要的类:
EGLDisplay:可以理解为要绘制的地方的一个抽象
EGLConfig:它是EGL上下文的配置参数,比如RGBA的位宽等等
EGLContext:代表EGL上下文
EGLSurface:opengl es渲染结果的缓冲区;opengl es通过它呈现到屏幕上或者实现离屏渲染
有人可能会问SurfaceView中的Surface是不是就是这里的EGLSurface,答案是。不过通过EGLSurface来操作渲染结果更加灵活,所以下面都是用EGLSurface来操作渲染结果;

EGL环境的搭建步骤:
-创建 EGLDisplay

private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
private void initEGLDisplay() {
        // 用于绘制的地方的一个抽闲
        mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
            MLog.log("eglGetDisplay fail");
        }

        // 初始化EGLDisplay,还可以在这个函数里面获取版本号
        boolean ret = EGL14.eglInitialize(mEGLDisplay,null,0,null,0);
        if (!ret) {
            MLog.log("eglInitialize fail");
        }
    }

eglGetDisplay()函数选择EGLDisplay类型,选择好了之后,在调用eglInitialize()进行初始化

-创建EGLConfig配置
// 定义 EGLConfig 属性配置,这里定义了红、绿、蓝、透明度、深度、模板缓冲的位数,属性配置数组要以EGL14.EGL_NONE结尾

private static final int[] EGL_CONFIG = {
            EGL14.EGL_RED_SIZE, 8,
            EGL14.EGL_GREEN_SIZE, 8,
            EGL14.EGL_BLUE_SIZE, 8,
            EGL14.EGL_ALPHA_SIZE, 8,
            EGL14.EGL_DEPTH_SIZE, 16,
            EGL14.EGL_STENCIL_SIZE, 0,
            EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
            EGL14.EGL_NONE,
    };
    private EGLConfig mEGLConfig;
    private void initEGLConfig() {
        // 所有符合配置的 EGLConfig 个数
        int[] numConfigs = new int[1];
        // 所有符合配置的 EGLConfig
        EGLConfig[] configs = new EGLConfig[1];

        // 会获取所有满足 EGL_CONFIG 的 config,然后取第一个
        EGL14.eglChooseConfig(mEGLDisplay, EGL_CONFIG, 0, configs, 0, configs.length, numConfigs, 0);
        mEGLConfig = configs[0];
    }

eglChooseConfig()函数将选择一个满足配置的EGLconfig

-创建上下文EGLContext

private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
private static final int[] EGLCONTEXT_ATTRIBUTE = {
            EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL14.EGL_NONE,
    };
    private void initEGLContext() {
        // 创建上下文
        mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfig, EGL14.EGL_NO_CONTEXT, EGLCONTEXT_ATTRIBUTE, 0);
        if (mEGLContext == EGL14.EGL_NO_CONTEXT) {
            MLog.log("eglCreateContext fail");
        }
    }

-创建EGLSurface
EGLSurface分两种,一种是将渲染结果呈现到屏幕上;另外一种是将渲染结果作为离线缓存不呈现到屏幕上,所以创建方法也不一样;
第一种:

public void createWindowSurface(Object surface) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglContext.createWindowSurface(surface);
    }

这里的surface变量为SurfaceTexture类型和Surface类型都可以。所以这里可以明白我们其实可以再次创建一个EGLSurface类操作SurfaceView中的Surface的路径了吧,没错,就是通过该方法

第二种:

public void createOffscreenSurface(int width, int height) {
        if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
            throw new IllegalStateException("surface already created");
        }
        mEGLSurface = mEglContext.createOffscreenSurface(width, height);
        mWidth = width;
        mHeight = height;
    }

创建离线渲染的EGLSurface,与前面不同的是,这里并不需要surface,只需要指定宽高即可。

至此EGL环境搭建流程全部完毕,具体可以参考
参考 google的示例代码 grafika 地址:https://github.com/google/grafika/

2、实现渲染线程
这里用java的Thread类实现,通过继承Thread类
private class RenderThread extends Thread implements SurfaceHolder.Callback {
......
}
这里先讲一下SurfaceHolder.Callback这个接口,它代表了SurfaceView内部的那个Surface从创建到变化到销毁的整个生命周期,如下:

public interface Callback {
        public void surfaceCreated(SurfaceHolder holder);
        public void surfaceChanged(SurfaceHolder holder, int format, int width,
                int height);
        public void surfaceDestroyed(SurfaceHolder holder);
    }

这几个回调函数意义我就不一一讲解了,看名字也可以知道。这里我只是讲一下为什么自己实现的渲染线程要实现这个接口呢?其实不一定要渲染线程实现,也可以在MySurfaceView类实现,只要实现该接口即可。实现的目的在于:
-我们可通过该Surface对整个EGL的生命周期进行管理
-拿到该Surface后,我们可以把它作为EGLSurface的参数来创建一个EGL对象,这样他们是共享同一个渲染缓冲区的,有利于节约内存。

接下来回到渲染线程的实现。其实渲染线程主要的工作就是调用opengl es的指令进行渲染,然后将渲染结果根据需求是呈现到屏幕上还是离线放到渲染缓冲区
我们这里实现的非常简单,渲染线程开启后调用通过ondraw()函数调用一次opengl es指令然后就关闭该线程(具体实现可以参考GLSurfaceView)
首先看一下渲染线程实现的SurfaceHolder.Callback接口

@Override
        public void surfaceCreated(SurfaceHolder holder) {
            MLog.log("surfaceCreated 创建了");
            synchronized (mLock) {
                mSurfaceTexture = holder.getSurface();
                mLock.notify();
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            MLog.log("surfaceChanged 创建了");
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            MLog.log("surfaceDestroyed 创建了");
        }

这些接口都是在UI线程调用的,因为我们的渲染线程执行顺序可能在这个函数之前,为了保证渲染线程中EGLSurface 拿到的Surface不为空,这里用锁和条件变量方式实现,如上

然后是渲染线程

@Override
        public void run() {
            Surface st = null;
            mRun = true;
            while (true) {
                // 等待Surface的Surface创建成功
                synchronized (mLock) {
                    while (mRun && (st = mSurfaceTexture) == null) {

                        try {
                            // 阻塞当前线程,直到在其它线程调用mLock.notifyAll()/mLock.notify()函数
                            MLog.log("texutre 还未创建,等待");
                            mLock.wait();
                        } catch (InterruptedException ie) {
                            ie.printStackTrace();
                        }

                        if (!mRun) {
                            break;
                        }
                    }
                }
                MLog.log("开始渲染 ");
                // 获取到了SurfaceTexture,那么开始做渲染工作
                mGLcontext = new GLContext();
                mSurface = new GLSurface(mGLcontext);

                /** 遇到问题:
                 * 奔溃:
                 * eglCreateWindowSurface: native_window_api_connect (win=0x75d7e8d010) failed (0xffffffed) (already connected to another API?)
                 * 解决方案:
                 * 因为SurfaceTexture还未与前面的EGLContext解绑就又被绑定到其它EGLContext,导致奔溃。原因就是下面渲染结束后没有跳出循环;在while语句最后添加
                 * break;语句
                 * */
                mSurface.createWindowSurface(st);

                mSurface.makeCurrentReadFrom(mSurface);

                onSurfaceCreated();

                onDraw();

                /** 遇到问题:渲染结果没有成功显示到屏幕上
                 * 解决方案:因为下面提前释放了SurfaceTexture导致的问题。不应该在这里释放
                 * */
                // 渲染结束 进行相关释放工作
//                st.release();

                MLog.log("渲染结束");
                finishRender = true;

                break;
            }
        }

可以看到,渲染线程在收到SurfaceView的Surface被初始化后的锁通知前一直休眠,直到通知到达才开始EGL环境的初始化

接下来初始化EGL,
接下来ondraw()函数调用
那么opengl es的指令就可以像GLSurfaceView那样写在ondraw函数中了。

private void onDraw() {
....
// 必须要有,否则渲染结果不会呈现到屏幕上
mSurface.swapBuffers();
}

注意:前面我们使用GLSurfaceView时是没有调用swapBuffers()这个函数的,没错,因为GLSurfaceView内部自动调用了,但是这里我们要自己调用,最终的渲染结果才会呈现到屏幕上。

至此整个SurfaceView搭建opengl es环境流程就完了。

TextureView搭建opengl es环境

TextureView和SurfaceView不同的是,它内部没有Surface,但是有SurfaceTexture,前面我们知道以SurfaceTexture作为参数可以创建EGLSurface,同样需要我们自己实现EGL管理和渲染线程的管理。
其实EGL的实现和渲染线程的实现和SurfaceView差不多,这里只是讲一下不同的地方

渲染线程实现SurfaceTextureListener接口,该接口如下:

public static interface SurfaceTextureListener {
  public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height);
  public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height);
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface);
public void onSurfaceTextureUpdated(SurfaceTexture surface);
}

咋一看是不是与SurfaceView的SurfaceHolder.Callback接口神似,没错,该接口就是TextureView内部的SurfaceTexture的生命周期。那后面的渲染线程我就不在讲解了,依然是又渲染线程实现SurfaceTextureListener接口,

GLSurfaceView、SurfaceView、TextureView区别总结:

1、我们可以知道GLSurfaceView实现opengl es是最简单的了,我们只需要调用opengl es 函数指令即可,其它都交给GLSurfaceView来实现
2、SurfaceView和TextureView实现opengl es环境流程其实大体一样,至于他们效率上的区别这里就不做讨论了,因为笔者也没有认真去研究。后面研究透了在单独来写
3、GLSurfaceView内部实现的是渲染到屏幕上的EGLSurface,所以这也是它的缺点

渲染一张图片到屏幕上

前面opengl es的环境搭建好了,那么接下来我们做点正事了。如何渲染一张图片到屏幕上,这里就涉及到opengl es的使用流程了,前面的opengl es基础文字中我们知道,opengl es的渲染管线分为七个步骤,但实际上提供给app的只有其中几个步骤,下面一一道来
1、搭建opengl es环境
参考前面,这里用前面TextureView搭建的环境为例来实现
2、初始化顶点坐标和着色器程序
顶点坐标的初始化可以在onSurfaceCreated()回调中进行,当然你也可以在onDraw()回调中,都可以。

// 顶点坐标
private static final float verdata[] = {
-1.0f,-1.0f, // 左下角
1.0f,-1.0f, // 右下角
-1.0f,1.0f, // 左上角
1.0f,1.0f // 右上角
};
// 纹理坐标
private static final float texdata[] = {
0.0f,1.0f, // 左下角
1.0f,1.0f, // 右上角
0.0f,0.0f, // 左上角
1.0f,0.0f, // 右上角
};

前面我们的opengl es基础文章中,我们知道,渲染管线中有一步骤是要确定顶点坐标,纹理坐标,顶点坐标规则和纹理坐标

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
    // 初始化着色器程序
    mprogram = new GLProgram(vString,fString);
    mprogram.useprogram()

    // 初始化顶点坐标和纹理坐标v
    vbuffer = ByteBuffer.allocateDirect(verdata.length * 4);
    vbuffer.order(ByteOrder.nativeOrder())
    .asFloatBuffer().put(verdata)
    .position(0);
    fbuffer = ByteBuffer.allocateDirect(texdata.length * 4);
    fbuffer.order(ByteOrder.nativeOrder())
    .asFloatBuffer().put(texdata)
    .position(0);
}

这里要说明的就是,java中是大端序,而opengl es中数据是小端序,所以这里
order(ByteOrder.nativeOrder())将大端序转换成小端序。
GLProgram是我封装的专门用于处理着色器程序的类,这里将顶点着色器和片段着色器代码作为参数创建一个着色器程序
其实着色器程序的源码加载,编译,连接,加载程序有一个固定的流程,所以就可以封装了,这里不在多讲,具体实现可以参考项目代码。
3、将顶点坐标,纹理坐标传递给opengl es

GLES20.glViewport(0,0,width,height);
GLES20.glClearColor(1.0f,0,0,1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

// 为着色器程序赋值
mprogram.useprogram();

int position = mprogram.attributeLocationForname("position");
int texcoord = mprogram.attributeLocationForname("texcoord");
int texture = mprogram.uniformaLocationForname("texture");
GLES20.glVertexAttribPointer(position,2,GLES20.GL_FLOAT,false,0,vbuffer);
GLES20.glEnableVertexAttribArray(position);
GLES20.glVertexAttribPointer(texcoord,2,GLES20.GL_FLOAT,false,0,fbuffer);
GLES20.glEnableVertexAttribArray(texcoord);
MLog.log("position " + position + " texcoord " + texcoord + " texture " + texture);

注意:使用着色器程序之前,一定要先调用着色器程序
mprogram.useprogram();

4、上传纹理

// 开始上传纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 设置纹理参数
IntBuffer texIntbuffer = IntBuffer.allocate(1);
GLES20.glGenTextures(1,texIntbuffer);
texture = texIntbuffer.get(0);
if (texture == 0) {
    MLog.log("glGenTextures fail 0");
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture);

// 设置纹理参数
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);

// 第二个参数和前面用glActiveTexture()函数激活的纹理单元编号要一致,这样opengl es才知道用哪个纹理单元对象 去处理纹理
GLES20.glUniform1i(texture,0);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,mBitmap,0);

注意的地方就是:
-上传纹理之前一定要先调用glActiveTexture()激活当前单元;调用glBindTexture()绑定当前texture id
-调用GLES20.glUniform1i(texture,0);将当前纹理单元id传递给片元着色器中纹理变量
-GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,mBitmap,0);是google提供的java层的上传bitmap到opengl es的工具,opengl es api中没有该函数,但是最终是调用glTexImage2D()函数
这里讲一下该函数。它使用GL_RGBA作为glTexImage2d()的internalformat和format参数,使用GL_UNSIGNED_BYTE作为type参数。所以如果传递的bitmap不是RGBA格式,这里会出错,那么就可以用
public static void texImage2D(int target, int level, int internalformat,
Bitmap bitmap, int type, int border);这个函数了
指定bitmap对应的像素格式和type了

5、渲染并将渲染结果呈现到屏幕上

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);

// 必须要有,否则渲染结果不会呈现到屏幕上
mSurface.swapBuffers();

以上就是如何基于TextureView用opengl es渲染一张图片到屏幕上

项目地址

https://github.com/nldzsz/opengles-android
1、GLSurfaceView搭建opengl es环境请参考MyGLSurfaceView类
2、SurfaceView搭建opengl es环境请参考MySurfaceView类
3、TextureView搭建opengl es环境请参考MyTextureView类
4、opengl es渲染一张图片到屏幕上请参考MySurfaceView和MyTextureView

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐