百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 博客教程 > 正文

用 OpenGL 画一个三角形丨音视频工程示例

connygpt 2024-11-18 10:41 5 浏览

渲染是音视频技术栈相关的一个非常重要的方向,视频图像在设备上的展示、各种流行的视频特效都离不开渲染技术的支持。

在 RenderDemo 这个工程示例系列,我们将为大家展示一些渲染相关的 Demo,来向大家介绍如何在 iOS/Android 平台上手一些渲染相关的开发。

这里是第一篇:用 OpenGL 画一个三角形。我们分别在 iOS 和 Android 实现了用 OpenGL 画一个三角形的 Demo。在本文中,包括如下内容:

  • 1)iOS OpenGL 绘制三角形 Demo;
  • 2)Android OpenGL 绘制三角形 Demo;
  • 3)详尽的代码注释,帮你理解代码逻辑和原理。

如果我们了解了 OpenGL ES 就会知道,虽然它定义了一套移动设备的图像渲染 API,但是并没有定义窗口系统。为了让 GLES 能够适配各种平台,GLES 需要与知道如何通过操作系统创建和访问窗口的库结合使用,这就有了 EGL,EGL 是 OpenGL ES 渲染 API 和本地窗口系统之间的一个中间接口层,它主要由系统制造商实现。EGL 提供如下机制:

  • 与设备的原生窗口系统通信;
  • 查询绘图表面的可用类型和配置;
  • 创建绘图表面;
  • 在 OpenGL ES 和其他图形渲染 API 之间同步渲染;
  • 管理纹理贴图等渲染资源。

EGL 是 OpenGL ES 与设备的桥梁,以实现让 OpenGL ES 能够在当前设备上进行绘制。

领音视频开发资料→音视频开发(资料文档+视频教程+项目源码)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

1、iOS Demo

iOS 平台对 EGL 的实现是 EAGL(Embedded Apple Graphics Library),其中 CAEAGLLayer 就是一种可以支持 OpenGL ES 绘制的图层类型,我们的 Demo 里会用它作为绘制三角形的图层。

代码比较简单,我们直接上代码:

DMTriangleRenderView.m

#import "DMTriangleRenderView.h"
#import <OpenGLES/ES2/gl.h>

// 定义顶点的数据结构:包括顶点坐标和颜色维度。
#define PositionDimension 3
#define ColorDimension 4
typedef struct {
    GLfloat position[PositionDimension]; // { x, y, z }
    GLfloat color[ColorDimension]; // {r, g, b, a}
} SceneVertex;


@interface DMTriangleRenderView ()
@property (nonatomic, assign) GLsizei width;
@property (nonatomic, assign) GLsizei height;

@property (nonatomic, strong) CAEAGLLayer *eaglLayer;
@property (nonatomic, strong) EAGLContext *eaglContext;

@property (nonatomic, assign) GLuint simpleProgram;

@property (nonatomic, assign) GLuint renderBuffer;
@property (nonatomic, assign) GLuint frameBuffer;

@end

@implementation DMTriangleRenderView

#pragma mark - Setup
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _width = frame.size.width;
        _height = frame.size.height;
        [self render];
    }
    return self;
}

#pragma mark - Action
- (void)render {
    // 1、设定 layer 的类型。
    _eaglLayer = (CAEAGLLayer *) self.layer;
    _eaglLayer.opacity = 1.0;
    _eaglLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking: @(NO),
                                      kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};

    
    // 2、创建 OpenGL 上下文。
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2; // 使用的 OpenGL API 的版本。
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:api];
    if (!context) {
        NSLog(@"Create context failed!");
        return;
    }
    BOOL r = [EAGLContext setCurrentContext:context];
    if (!r) {
        NSLog(@"setCurrentContext failed!");
        return;
    }
    _eaglContext = context;

    
    // 3、申请并绑定渲染缓冲区对象 RBO 用来存储即将绘制到屏幕上的图像数据。
    glGenRenderbuffers(1, &_renderBuffer); // 创建 RBO。
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer); // 绑定 RBO 到 OpenGL 渲染管线。
    [_eaglContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer]; // 将渲染图层(_eaglLayer)的存储绑定到 RBO。

    
    // 4、申请并绑定帧缓冲区对象 FBO。FBO 本身不能用于渲染,只有绑定了纹理(Texture)或者渲染缓冲区(RBO)等作为附件之后才能作为渲染目标。
    glGenFramebuffers(1, &_frameBuffer); // 创建 FBO。
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer); // 绑定 FBO 到 OpenGL 渲染管线。
    // 将 RBO 绑定为 FBO 的一个附件,绑定后,OpenGL 对 FBO 的绘制会同步到 RBO 后再上屏。
    glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                              GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER,
                              _renderBuffer);

    
    // 5、清理窗口颜色,并设置渲染窗口。
    glClearColor(0.5, 0.5, 0.5, 1.0); // 设置渲染窗口颜色。这里是灰色。
    glClear(GL_COLOR_BUFFER_BIT); // 清空旧渲染缓存。
    glViewport(0, 0, _width, _height); // 设置渲染窗口区域。


    // 6、加载和编译 shader,并链接到着色器程序。
    if (_simpleProgram) {
        glDeleteProgram(_simpleProgram);
        _simpleProgram = 0;
    }
    // 加载和编译 shader。
    NSString *simpleVSH = [[NSBundle mainBundle] pathForResource:@"simple" ofType:@"vsh"];
    NSString *simpleFSH = [[NSBundle mainBundle] pathForResource:@"simple" ofType:@"fsh"];
    _simpleProgram = [self loadShaderWithVertexShader:simpleVSH fragmentShader:simpleFSH];
    // 链接 shader program。
    glLinkProgram(_simpleProgram);
    // 打印链接日志。
    GLint linkStatus;
    glGetProgramiv(_simpleProgram, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(_simpleProgram, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetProgramInfoLog(_simpleProgram, infoLength, NULL, infoLog);
            NSLog(@"%s", infoLog);
            free(infoLog);
        }
    }
    glUseProgram(_simpleProgram);
    
    
    // 7、根据三角形顶点信息申请顶点缓冲区对象 VBO 和拷贝顶点数据。
    // 设置三角形 3 个顶点数据,包括坐标信息和颜色信息。
    const SceneVertex vertices[] = {
        {{-0.5,  0.5, 0.0}, { 1.0, 0.0, 0.0, 1.000}}, // 左下 // 红色
        {{-0.5, -0.5, 0.0}, { 0.0, 1.0, 0.0, 1.000}}, // 右下 // 绿色
        {{ 0.5, -0.5, 0.0}, { 0.0, 0.0, 1.0, 1.000}}, // 左上 // 蓝色
    };
    // 申请并绑定 VBO。VBO 的作用是在显存中提前开辟好一块内存,用于缓存顶点数据,从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以提升渲染性能。
    GLuint vertexBufferID;
    glGenBuffers(1, &vertexBufferID); // 创建 VBO。
    glBindBuffer(GL_ARRAY_BUFFER, vertexBufferID); // 绑定 VBO 到 OpenGL 渲染管线。
    // 将顶点数据 (CPU 内存) 拷贝到 VBO(GPU 显存)。
    glBufferData(GL_ARRAY_BUFFER, // 缓存块类型。
                 sizeof(vertices), // 创建的缓存块尺寸。
                 vertices, // 要绑定的顶点数据。
                 GL_STATIC_DRAW); // 缓存块用途。
    
    // 8、绘制三角形。
    // 获取与 Shader 中对应的参数信息:
    GLuint vertexPositionLocation = glGetAttribLocation(_simpleProgram, "v_position");
    GLuint vertexColorLocation = glGetAttribLocation(_simpleProgram, "v_color");
    // 顶点位置属性。
    glEnableVertexAttribArray(vertexPositionLocation); // 启用顶点位置属性通道。
    // 关联顶点位置数据。
    glVertexAttribPointer(vertexPositionLocation, // attribute 变量的下标,范围是 [0, GL_MAX_VERTEX_ATTRIBS - 1]。
                          PositionDimension, // 指顶点数组中,一个 attribute 元素变量的坐标分量是多少(如:position, 程序提供的就是 {x, y, z} 点就是 3 个坐标分量)。
                          GL_FLOAT, // 数据的类型。
                          GL_FALSE, // 是否进行数据类型转换。
                          sizeof(SceneVertex), // 每一个数据在内存中的偏移量,如果填 0 就是每一个数据紧紧相挨着。
                          (const GLvoid*) offsetof(SceneVertex, position)); // 数据的内存首地址。
    // 顶点颜色属性。
    glEnableVertexAttribArray(vertexColorLocation);
    // 关联顶点颜色数据。
    glVertexAttribPointer(vertexColorLocation,
                          ColorDimension,
                          GL_FLOAT,
                          GL_FALSE,
                          sizeof(SceneVertex),
                          (const GLvoid*) offsetof(SceneVertex, color));
    // 绘制所有图元。
    glDrawArrays(GL_TRIANGLES, // 绘制的图元方式。
                 0, // 从第几个顶点下标开始绘制。
                 sizeof(vertices) / sizeof(vertices[0])); // 有多少个顶点下标需要绘制。
    // 把 Renderbuffer 的内容显示到窗口系统 (CAEAGLLayer) 中。
    [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
    
    
    // 9、清理。
    glDisableVertexAttribArray(vertexColorLocation); // 关闭顶点颜色属性通道。
    glDisableVertexAttribArray(vertexPositionLocation); // 关闭顶点位置属性通道。
    glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑 VBO。
    glBindFramebuffer(GL_FRAMEBUFFER, 0); // 解绑 FBO。
    glBindRenderbuffer(GL_RENDERBUFFER, 0); // 解绑 RBO。
}

#pragma mark - Utility
- (GLuint)loadShaderWithVertexShader:(NSString *)vert fragmentShader:(NSString *)frag {
    GLuint verShader, fragShader;
    GLuint program = glCreateProgram(); // 创建 Shader Program 对象。
    
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    // 装载 Vertex Shader 和 Fragment Shader。
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file {
    NSString *content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar *source = (GLchar *) [content UTF8String];
    *shader = glCreateShader(type); // 创建一个着色器对象。
    glShaderSource(*shader, 1, &source, NULL); // 关联顶点、片元着色器的代码。
    glCompileShader(*shader); // 编译着色器代码。
    
    // 打印编译日志。
    GLint compileStatus;
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &compileStatus);
    if (compileStatus == GL_FALSE) {
        GLint infoLength;
        glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetShaderInfoLog(*shader, infoLength, NULL, infoLog);
            NSLog(@"%s -> %s", (type == GL_VERTEX_SHADER) ? "vertex shader" : "fragment shader", infoLog);
            free(infoLog);
        }
    }
}

#pragma mark - Override
+ (Class)layerClass {
    return [CAEAGLLayer class];
}

@end

上面的代码包括了这些过程:

  • 1)定义顶点的数据结构:包括顶点坐标和颜色维度;
  • 2)设定 layer 的类型;
  • 3)创建 OpenGL 上下文;
  • 4)申请并绑定渲染缓冲区对象 RBO 用来存储即将绘制到屏幕上的图像数据;
  • 5)申请并绑定帧缓冲区对象 FBO;
    • 需要注意,FBO 本身不能用于渲染,只有绑定了纹理(Texture)或者渲染缓冲区(RBO)等作为附件之后才能作为渲染目标。
  • 6)清理窗口颜色,并设置渲染窗口;
  • 7)加载和编译 shader,并链接到着色器程序;
  • 8)根据三角形顶点信息申请顶点缓冲区对象 VBO 和拷贝顶点数据;
    • 这里 VBO 的作用是在显存中提前开辟好一块内存,用于缓存顶点数据,从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以提升渲染性能。
  • 9)绘制三角形;
  • 10)清理状态机。

更具体细节见上述代码及其注释。

最终我们画出的三角形如下图所示:

2、Android Demo

Android 平台自 2.0 版本之后图形系统的底层渲染均由 OpenGL ES 负责,其 EGL 架构实现如下图所示:

  • Display 是对实际显示设备的抽象。在 Android 上的实现类是 EGLDisplay
  • Surface 是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer、Stencil Buffer、Depth Buffer。在 Android 上的实现类是 EGLSurface
  • Context 存储 OpenGL ES 绘图的一些状态信息。在 Android 上的实现类是 EGLContext

Android Demo 的代码由于平台 EGL 实现的原因以及对模块略作了封装,所以看起来对稍微复杂一些,包括了如下几个文件:

  • KFGLContext.java,KFGLContext 负责管理和组装 EGLDisplay、EGLSurface、EGLContext。
  • KFGLProgram.java,KFGLProgram 负责加载和编译着色器,创建着色器程序容器。
  • KFSurfaceView.java,KFSurfaceView 继承自 SurfaceView 来实现渲染。
  • KFTextureView.java,KFTextureView 继承自 TextureView 来实现渲染。
  • KFRenderView.java,KFRenderView 是一个容器,可以选择使用 KFSurfaceView 或 KFTextureView 作为实际的渲染视图。
  • KFRenderListener.java,KFRenderListener 是 KFRenderView 用来监听渲染视图的事件回调。

代码如下:

KFGLContext.java

// ...省略 import 代码...
public class KFGLContext {
    private Surface mSurface = null;
    private EGLDisplay mEGLDisplay = EGL_NO_DISPLAY; // 实际显示设备的抽象
    private EGLContext mEGLContext = EGL_NO_CONTEXT; // 渲染上下文
    private EGLSurface mEGLSurface = EGL_NO_SURFACE; // 存储图像的内存区域
    private EGLContext mEGLShareContext = EGL_NO_CONTEXT; // 共享渲染上下文
    private EGLConfig mEGLConfig = null; // 渲染表面的配置信息

    private EGLContext mLastContext = EGL_NO_CONTEXT; // 存储之前系统上下文
    private EGLDisplay mLastDisplay = EGL_NO_DISPLAY; // 存储之前系统设备
    private EGLSurface mLastSurface = EGL_NO_SURFACE; // 存储之前系统内存区域
    private boolean mIsBind = false;

    public KFGLContext(EGLContext shareContext) {
        mEGLShareContext = shareContext;
        // 创建 OpenGL 上下文
        _eglSetup();
    }

    public KFGLContext(EGLContext shareContext, Surface surface) {
        mEGLShareContext = shareContext;
        mSurface = surface;
        // 创建 OpenGL 上下文
        _eglSetup();
    }

    public void setSurface(Surface surface) {
        if (surface == null || surface == mSurface) {
            return;
        }

        // 释放渲染表面 Surface
        if (mEGLDisplay != EGL_NO_DISPLAY && mEGLSurface != EGL_NO_SURFACE) {
            eglDestroySurface(mEGLDisplay, mEGLSurface);
        }
        // 创建 Surface
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };
        mSurface = surface;
        mEGLSurface = eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mSurface, surfaceAttribs, 0);
        if (mEGLSurface == null) {
            throw new RuntimeException("surface was null");
        }
    }

    public EGLContext getContext() {
        return mEGLContext;
    }

    public Surface getSurface() {
        return mSurface;
    }

    public boolean swapBuffers() {
        // 将后台绘制的缓冲显示到前台
        if (mEGLDisplay != EGL_NO_DISPLAY && mEGLSurface != EGL_NO_SURFACE) {
            return eglSwapBuffers(mEGLDisplay, mEGLSurface);
        } else {
            return false;
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    public void setPresentationTime(long nsecs) {
        // 设置时间戳
        if (mEGLDisplay != EGL_NO_DISPLAY && mEGLSurface != EGL_NO_SURFACE) {
            eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);
        }
    }

    public void bind() {
        // 绑定当前上下文
        mLastSurface = eglGetCurrentSurface(EGL14.EGL_READ);
        mLastContext = eglGetCurrentContext();
        mLastDisplay = eglGetCurrentDisplay();
        if (!eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
            throw new RuntimeException("eglMakeCurrent failed");
        }
        mIsBind = true;
    }

    public void unbind() {
        if (!mIsBind) {
            return;
        }

        // 绑定回系统之前上下文
        if (mLastSurface != EGL_NO_SURFACE && mLastContext != EGL_NO_CONTEXT  && mLastDisplay != EGL_NO_DISPLAY) {
            eglMakeCurrent(mLastDisplay, mLastSurface, mLastSurface, mLastContext);
            mLastDisplay = EGL_NO_DISPLAY;
            mLastSurface = EGL_NO_SURFACE;
            mLastContext = EGL_NO_CONTEXT;
        } else {
            if (mEGLDisplay != EGL_NO_DISPLAY) {
                eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            }
        }
        mIsBind = false;
    }

    public void release() {
        unbind();

        // 释放设备、Surface
        if (mEGLDisplay != EGL_NO_DISPLAY && mEGLSurface != EGL_NO_SURFACE) {
            eglDestroySurface(mEGLDisplay, mEGLSurface);
        }

        if (mEGLDisplay != EGL_NO_DISPLAY && mEGLContext != EGL_NO_CONTEXT) {
            eglDestroyContext(mEGLDisplay, mEGLContext);
        }

        if(mEGLDisplay != EGL_NO_DISPLAY){
            eglTerminate(mEGLDisplay);
        }

        mSurface = null;
        mEGLShareContext = null;

        mEGLDisplay = null;
        mEGLContext = null;
        mEGLSurface = null;
    }

    private void _eglSetup() {
        // 创建设备
        mEGLDisplay = eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL_NO_DISPLAY) {
            throw new RuntimeException("unable to get EGL14 display");
        }

        int[] version = new int[2];
        // 根据版本初始化设备
        if (!eglInitialize(mEGLDisplay, version, 0, version, 1)) {
            mEGLDisplay = null;
            throw new RuntimeException("unable to initialize EGL14");
        }

        // 定义 EGLConfig 属性配置,定义红、绿、蓝、透明度、深度、模板缓冲的位数
        int[] attribList = {
                EGL14.EGL_BUFFER_SIZE, 32,
                EGL14.EGL_ALPHA_SIZE, 8, // 颜色缓冲区中透明度用几位来表示
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
                EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
                EGL14.EGL_NONE
        };
        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        // 找到符合要求的 EGLConfig 配置
        if (!eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
                numConfigs, 0)) {
            throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
        }
        mEGLConfig = configs[0];

        // 指定 OpenGL 使用版本
        int[] attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        // 创建 GL 上下文
        mEGLContext = eglCreateContext(mEGLDisplay, mEGLConfig, mEGLShareContext != null ? mEGLShareContext : EGL_NO_CONTEXT, attrib_list, 0);
        if (mEGLContext == null) {
            throw new RuntimeException("null context");
        }

        // 创建 Surface 配置信息
        int[] surfaceAttribs = {
                EGL14.EGL_NONE
        };

        // 创建 Surface
        if (mSurface != null) {
            mEGLSurface = eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mSurface,
                    surfaceAttribs, 0);
        } else {
            mEGLSurface = eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs, 0);
        }

        if (mEGLSurface == null) {
            throw new RuntimeException("surface was null");
        }
    }
}

KFGLProgram.java

// ...省略 import 代码...
public class KFGLProgram {
    private static final String TAG = "KFGLProgram";
    private int mProgram = 0; // 着色器程序容器
    private int mVertexShader = 0; // 顶点着色器
    private int mFragmentShader = 0; // 片元着色器

    public KFGLProgram(String vertexShader, String fragmentShader) {
        _createProgram(vertexShader,fragmentShader);
    }

    public void release() {
        // 释放顶点、片元着色器、着色器容器
        if (mVertexShader != 0) {
            glDeleteShader(mVertexShader);
            mVertexShader = 0;
        }

        if (mFragmentShader != 0) {
            glDeleteShader(mFragmentShader);
            mFragmentShader = 0;
        }

        if (mProgram != 0) {
            glDeleteProgram(mProgram);
            mProgram = 0;
        }
    }

    public void use() {
        // 使用当前的着色器
        if (mProgram != 0) {
            glUseProgram(mProgram);
        }
    }

    public int getUniformLocation(String uniformName) {
        // 获取着色器 uniform 对应下标
        return glGetUniformLocation(mProgram, uniformName);
    }

    public int getAttribLocation(String uniformName) {
        // 获取着色器 attribute 变量对应下标
        return glGetAttribLocation(mProgram, uniformName);
    }

    private void  _createProgram(String vertexSource, String fragmentSource) {
        // 创建着色器容器
        // 创建顶点、片元着色器
        mVertexShader = _loadShader(GLES20.GL_VERTEX_SHADER,   vertexSource);
        mFragmentShader = _loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);

        //
        if(mVertexShader != 0 && mFragmentShader != 0){
            // 创建一个空的着色器容器
            mProgram = GLES20.glCreateProgram();
            // 将顶点、片元着色器添加至着色器容器
            glAttachShader(mProgram, mVertexShader);
            glAttachShader(mProgram, mFragmentShader);

            // 链接着色器容器
            glLinkProgram(mProgram);
            int[] linkStatus = new int[1];
            glGetProgramiv(mProgram, GLES20.GL_LINK_STATUS, linkStatus, 0);
            // 获取链接状态
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e(TAG, "Could not link program:");
                Log.e(TAG, glGetProgramInfoLog(mProgram));
                glDeleteProgram(mProgram);
                mProgram = 0;
            }
        }
    }

    private int _loadShader(int shaderType, String source) {
        // 根据类型创建顶点、片元着色器
        int shader = glCreateShader(shaderType);
        // 设置着色器中的源代码
        glShaderSource(shader, source);
        // 编译着色器
        glCompileShader(shader);

        int[] compiled = new int[1];
        glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
        // 获取编译后状态
        if (compiled[0] != GLES20.GL_TRUE) {
            Log.e(TAG, "Could not compile shader(TYPE=" + shaderType + "):");
            Log.e(TAG, glGetShaderInfoLog(shader));
            glDeleteShader(shader);
            shader = 0;
        }

        return shader;
    }
}

KFSurfaceView.java

// ...省略 import 代码...
public class KFSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private KFRenderListener mListener = null; // 回调
    private SurfaceHolder mHolder = null; // Surface 的抽象接口

    public KFSurfaceView(Context context, KFRenderListener listener) {
        super(context);
        mListener = listener;
        getHolder().addCallback(this);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
        // Surface 创建
        mHolder = surfaceHolder;
        // 根据 SurfaceHolder 创建 Surface
        if(mListener != null){
            mListener.surfaceCreate(surfaceHolder.getSurface());
        }
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) {
        // Surface 分辨率变更
        if(mListener != null){
            mListener.surfaceChanged(surfaceHolder.getSurface(),width,height);
        }
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
        // Surface 销毁
        if (mListener != null) {
            mListener.surfaceDestroy(surfaceHolder.getSurface());
        }
    }
}

KFTextureView.java

// ...省略 import 代码...
public class KFTextureView extends TextureView implements TextureView.SurfaceTextureListener{
    private KFRenderListener mListener = null; // 回调
    private Surface mSurface = null; // 渲染缓存
    private SurfaceTexture mSurfaceTexture = null; // 纹理缓存

    public KFTextureView(Context context, KFRenderListener listener) {
        super(context);
        this.setSurfaceTextureListener(this);
        mListener = listener;
    }

    @Override
    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
        // 纹理缓存创建
        mSurfaceTexture = surfaceTexture;
        // 根据 SurfaceTexture 创建 Surface
        mSurface = new Surface(surfaceTexture);
        if (mListener != null) {
            // 创建时候回调一次分辨率变更,对应 SurfaceView 接口
            mListener.surfaceCreate(mSurface);
            mListener.surfaceChanged(mSurface,width,height);
        }
    }

    @Override
    public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) {
        // 纹理缓存变更分辨率
        if (mListener != null) {
            mListener.surfaceChanged(mSurface,width,height);
        }
    }

    @Override
    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) {
        // 纹理缓存销毁
        if (mListener != null) {
            mListener.surfaceDestroy(mSurface);
        }
        if (mSurface != null) {
            mSurface.release();
            mSurface = null;
        }
        return false;
    }
}

KFRenderView.java

// ...省略 import 代码...
public class KFRenderView extends ViewGroup {
    // 顶点着色器
    public static String customVertexShader =
                    "attribute vec4 v_position;\n" +
                    "attribute vec4 v_color;\n" +
                    "varying mediump vec4 f_color;\n" +
                    "void main() {\n" +
                    "  f_color = v_color;\n" +
                    "  gl_Position = v_position;\n" +
                    "}\n";

    // 片元着色器
    public static String customFragmentShader =
                    "varying mediump vec4 f_color;\n" +
                    "void main() {\n" +
                    "  gl_FragColor = f_color;\n" +
                    "}\n";

    private KFGLContext mEGLContext = null; // OpenGL 上下文
    private EGLContext mShareContext = null; // 共享上下文
    private View mRenderView = null; // 渲染视图基类
    private int mSurfaceWidth = 0; // 渲染缓存宽
    private int mSurfaceHeight = 0; // 渲染缓存高
    private boolean mSurfaceChanged = false; // 渲染缓存是否变更
    private KFGLProgram mProgram;
    private FloatBuffer mVerticesBuffer = null; // 顶点 buffer
    private int mPositionAttribute = -1; // 顶点坐标
    private int mColorAttribute = -1; // 顶点颜色

    public KFRenderView(Context context, EGLContext eglContext) {
        super(context);
        mShareContext = eglContext; // 共享上下文

        // 1、选择实际的渲染视图
        boolean isSurfaceView  = false; // TextureView 与 SurfaceView 开关
        if (isSurfaceView) {
            mRenderView = new KFSurfaceView(context, mListener);
        } else {
            mRenderView = new KFTextureView(context, mListener);
        }

        this.addView(mRenderView); // 添加视图到父视图
    }

    public void release() {
        // 释放 OpenGL 上下文、特效
        if (mEGLContext != null) {
            mEGLContext.bind();
            mEGLContext.unbind();

            mEGLContext.release();
            mEGLContext = null;
        }
    }

    public void render() {
        mProgram.use();

        // 设置帧缓存背景色
        glClearColor(0.5f,0.5f,0.5f,1);
        // 清空帧缓存颜色
        glClear(GLES20.GL_COLOR_BUFFER_BIT);
        // 设置渲染窗口区域
        GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight);

        // 启用顶点着色器顶点坐标属性
        glEnableVertexAttribArray(mPositionAttribute);
        mVerticesBuffer.position(0); // 定位到第一个位置分量
        glVertexAttribPointer(
                mPositionAttribute,
                3, // x, y, z 有 3 个分量
                GLES20.GL_FLOAT,
                false,
                7 * 4, // 每个顶点有 xyzrgba 7 个分量,每个分量是 4 字节,所以步进为 7 * 4 字节
                mVerticesBuffer);

        // 启用顶点着色器顶点颜色属性
        glEnableVertexAttribArray(mColorAttribute);
        mVerticesBuffer.position(3); // 定位到第一个颜色分量
        glVertexAttribPointer(
                mColorAttribute,
                4, // r, g, b, a 有 4 个分量
                GLES20.GL_FLOAT,
                false,
                7 * 4, // 每个顶点有 xyzrgba 7 个分量,每个分量是 4 字节,所以步进为 7 * 4 字节
                mVerticesBuffer);

        // 绘制三角形
        glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

        // 关闭顶点着色器顶点坐标属性
        glDisableVertexAttribArray(mPositionAttribute);

        // 关闭顶点着色器顶点颜色属性
        glDisableVertexAttribArray(mColorAttribute);

        mEGLContext.swapBuffers();
    }

    private KFRenderListener mListener = new KFRenderListener() {
        @Override
        // 渲染缓存创建
        public void surfaceCreate(@NonNull Surface surface) {
            // 2、创建 OpenGL 上下文
            mEGLContext = new KFGLContext(mShareContext, surface);
            mEGLContext.bind();
            // 3、初始化 GL 相关环境:加载和编译 shader、链接到着色器程序、设置顶点数据
            _setupGL();
            mEGLContext.unbind();
        }

        @Override
        // 渲染缓存变更
        public void surfaceChanged(@NonNull Surface surface, int width, int height) {
            mSurfaceWidth = width;
            mSurfaceHeight = height;
            mSurfaceChanged = true;
            mEGLContext.bind();
            // 4、设置 OpenGL 上下文 Surface
            mEGLContext.setSurface(surface);
            // 5、绘制三角形
            render();
            mEGLContext.unbind();
        }

        @Override
        public void surfaceDestroy(@NonNull Surface surface) {

        }
    };

    private void _setupGL() {
        // 加载和编译 shader,并链接到着色器程序
        mProgram = new KFGLProgram(customVertexShader, customFragmentShader);

        // 获取与 shader 中对应的属性信息
        mPositionAttribute = mProgram.getAttribLocation("v_position");
        mColorAttribute = mProgram.getAttribLocation("v_color");

        // 3 个顶点,每个顶点有 7 个分量:x, y, z, r, g, b, a
        final float vertices[] = {
                -0.5f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, 1.0f, // 左下 // 红色
                -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, 1.0f, // 右下 // 绿色
                 0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f, 1.0f, // 左上 // 蓝色
        };
        ByteBuffer verticesByteBuffer = ByteBuffer.allocateDirect(4 * vertices.length);
        verticesByteBuffer.order(ByteOrder.nativeOrder());
        mVerticesBuffer = verticesByteBuffer.asFloatBuffer();
        mVerticesBuffer.put(vertices);
        mVerticesBuffer.position(0);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // 视图变更 Size
        this.mRenderView.layout(left, top, right, bottom);
    }
}

KFRenderListener.java

// ...省略 import 代码...
public interface KFRenderListener {
    void surfaceCreate(@NonNull Surface surface); // 渲染缓存创建
    void surfaceChanged(@NonNull Surface surface, int width, int height); // 渲染缓存变更分辨率
    void surfaceDestroy(@NonNull Surface surface); // 渲染缓存销毁
}

其中绘制三角形的代码实现在 KFRenderView.java 中,包括这些过程:

  • 1)选择实际的渲染视图;
  • 2)创建 OpenGL 上下文;
  • 3)初始化 GL 相关环境:加载和编译 shader、链接到着色器程序、设置顶点数据;
  • 4)设置 OpenGL 上下文 Surface;
  • 5)绘制三角形。

具体细节见上述代码及其注释。

最终我们画出的三角形如下图所示:

相关推荐

自学Python,写一个挨打的游戏代码来初识While循环

自学Python的第11天。旋转~跳跃~,我~闭着眼!学完循环,沐浴着while的光芒,闲来无事和同事一起扯皮,我说:“编程语言好神奇,一个小小的循环,竟然在生活中也可以找到原理和例子”,同事也...

常用的 Python 工具与资源,你知道几个?

最近几年你会发现,越来越多的人开始学习Python,工欲善其事必先利其器,今天纬软小编就跟大家分享一些常用的Python工具与资源,记得收藏哦!不然下次就找不到我了。1、PycharmPychar...

一张思维导图概括Python的基本语法, 一周的学习成果都在里面了

一周总结不知不觉已经自学Python一周的时间了,这一周,从认识Python到安装Python,再到基本语法和基本数据类型,对于小白的我来说无比艰辛的,充满坎坷。最主要的是每天学习时间有限。只...

三日速成python?打工人,小心钱包,别当韭菜

随着人工智能的热度越来越高,许多非计算机专业的同学们也都纷纷投入到学习编程的道路上来。而Python,作为一种相对比较容易上手的语言,也越来越受欢迎。网络上各类网课层出不穷,各式广告令人眼花缭乱。某些...

Python自动化软件测试怎么学?路线和方法都在这里了

Python自动化测试是指使用Python编程语言和相关工具,对软件系统进行自动化测试的过程。学习Python自动化测试需要掌握以下技术:Python编程语言:学习Python自动化测试需要先掌握Py...

Python从放弃到入门:公众号历史文章爬取为例谈快速学习技能

这篇文章不谈江流所专研的营销与运营,而聊一聊技能学习之路,聊一聊Python这门最简单的编程语言该如何学习,我完成的第一个Python项目,将任意公众号的所有历史文章导出成PDF电子书。或许我这个Py...

【黑客必会】python学习计划

阅读Python文档从Python官方网站上下载并阅读Python最新版本的文档(中文版),这是学习Python的最好方式。对于每个新概念和想法,请尝试运行一些代码片段,并检查生成的输出。这将帮助您更...

公布了!2025CDA考试安排

CDA数据分析师报考流程数据分析师是指在不同行业中专门从事行业数据搜集、整理、分析依据数据作出行业研究评估的专业人员CDA证书分为1-3级,中英文双证就业面广,含金量高!!?报考条件:满18...

一文搞懂全排列、组合、子集问题(经典回溯递归)

原创公众号:【bigsai】头条号:程序员bigsai前言Hello,大家好,我是bigsai,longtimenosee!在刷题和面试过程中,我们经常遇到一些排列组合类的问题,而全排列、组合...

「西法带你学算法」一次搞定前缀和

我花了几天时间,从力扣中精选了五道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。467.环绕字符串中唯一的子字符串[1](中等)795.区...

平均数的5种方法,你用过几种方法?

平均数,看似很简单的东西,其实里面包含着很多学问。今天,分享5种经常会用到的平均数方法。1.算术平均法用到最多的莫过于算术平均法,考试平均分、平均工资等等,都是用到这个。=AVERAGE(B2:B11...

【干货收藏】如何最简单、通俗地理解决策树分类算法?

决策树(Decisiontree)是基于已知各种情况(特征取值)的基础上,通过构建树型决策结构来进行分析的一种方式,是常用的有监督的分类算法。决策树算法是机器学习中的一种经典算法,它通过一系列的规则...

面试必备:回溯算法详解

我们刷leetcode的时候,经常会遇到回溯算法类型题目。回溯算法是五大基本算法之一,一般大厂也喜欢问。今天跟大家一起来学习回溯算法的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~什么是回溯...

「机器学习」决策树——ID3、C4.5、CART(非常详细)

决策树是一个非常常见并且优秀的机器学习算法,它易于理解、可解释性强,其可作为分类算法,也可用于回归模型。本文将分三篇介绍决策树,第一篇介绍基本树(包括ID3、C4.5、CART),第二篇介绍Ran...

大话AI算法: 决策树

所谓的决策树算法,通俗的说就是建立一个树形的结构,通过这个结构去一层一层的筛选判断问题是否好坏的算法。比如判断一个西瓜是否好瓜,有20条西瓜的样本提供给你,让你根据这20条(通过机器学习)建立起...