美文网首页 移动 前端 Python Android JavaOpenGL
OpenGL (一)OpenGL ES 绘制基础

OpenGL (一)OpenGL ES 绘制基础

作者: zcwfeng | 来源:发表于2020-12-03 11:30 被阅读0次

    Open Graphics Library
    图形领域的工业标准,是一套与硬件无关的跨编程语言、跨平台的、专业的图形编程(软件)接口。它用于二维、三维图像,是一个功能强大,调用方便的底层图形库。

    OpenGL ES(OpenGL for Embedded Systems)
    针对手机、PDA和游戏主机等嵌入式设备而设计的OpenGL API 子集。

    在Android SDK与NDK中均有提供OpenGL ES的类库。所以我们可以借助Java、C/C++来使用OpenGL

    和面向对象不同,OpenGL 是个状态机,面向过程编程。代码量比较多,难度较大。但是现在流 行的各种美颜相机、短视频APP实现各种图像渲染效果都需要OpenGL来完成。

    图像处理流程一般的方式如下;

    图像处理流程.png

    很多图形处理的书都有这样一个经典的图

    OpenGL管线.png

    抓了一张图,还是说明一下,OpenGL管线,通俗 的说所有的图形都是由三角行构成,只要给定定点就好。
    把三角形细分成像素点,每个像素点放大,就是我们第三个Fragments。根据像素点的位置,用着色器上色。
    最后精工Fragment处理,行程图像。

    EGL

    OpenGL ES只是图形API,不负责管理(显示)窗口,窗口的管理交由各个设备自己来完成。OpenGL ES调用用于渲染纹理多边形,而 EGL 调用用于将渲染放到屏幕上。

    GLSurfaceView

    调用任何 OpenGL 函数前,必须已经创建了 OpenGL 上下文。Android中GLSurfaceView中会为我们初始化OpenGL上下文。

    内部启动一个子线程(GL线 程)来初始化ELG环境,并完成OpenGL的绘制。使用GLSurfaceView,我们只能在这个EGL线程调用 OpenGL函数。

    类关系.png

    继承至SurfaceView,它内嵌的Surface专门负责OpenGL渲染。

    • 管理Surface与EGL;
    • 允许自定义渲染器(render);
    • 支持按需渲染和连续渲染;

    GLThread定义在GLSurfaceView内部,我们怎么在这个线程中去执行我们编写的代码?
    GLSurfaceView并不是一创建就会启动GL线程初始化环境。你可以把GLSurfaceView当成普通的 SurfaceView来使用。但是如果我们需要使用OpenGL ES在GLSurfaceView中绘制,那么我们需要调 用:

    
    //使用OpenGL ES 2.0 context. 
    setEGLContextClientVersion(2); 
    //設置渲染回調接口
    renderer = new CameraRender(this);
    setRenderer(renderer);
    /**
     * 刷新方式:注意必须在setRenderer 后面设置
     * RENDERMODE_WHEN_DIRTY 手动刷新,調用requestRender()回调一次渲染器的onDraw方
     * 法;
     * RENDERMODE_CONTINUOUSLY 自动刷新,大概16ms自動回調一次渲染器的onDraw方法
     */
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    

    其中 CameraRender 是一个实现了 GLSurfaceView.Renderer 接口的类。这个接口定义为:

    public interface Renderer {
    //窗口画布准备完成
    void onSurfaceCreated(GL10 gl, EGLConfig config); 
    //画布发生改变(如:横竖切换)
    void onSurfaceChanged(GL10 gl, int width, int height); 
    //绘制
    void onDrawFrame(GL10 gl);
    }
    

    所谓的刷新就是回调这个接口的 onDrawFrame 方法让我们进行OpenGL ES的绘制调用。其实 GLSurfaceView的setRenderer方法源码实现为:

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

    可以看到,如果需要在GLSurfaceView中配置的ELG环境并使用OpenGL ES就必须设置渲染接口。这样 才会启动线程初始化OpenGL ES的环境。而渲染接口需要实现的方法就是在GLThread回调的。

    GLThread定义在GLSurfaceView内部,我们怎么在这个线程中去执行我们编写的代码?
    渲染接口Renderer的三个回调方法就是在该线程中回调的。

    下面我们需要使用OpenGL ES在GLSurfaceView中显示摄像头的图像。 这里在之前的文章有说过
    请转到 RTMP(四)交叉编译与CameraX 结尾有提到

    Renderer渲染
    前面我们说了,我们需要在GLThread中去调用OpenGL ES的方法来完成渲染。因此我们的主要工作就 是在实现了Renderer接口的类中我们需要实现的: onSurfaceCreated 、 onSurfaceChanged 与 onDrawFrame 三个方法中完成绘制。

    onSurfaceCreated
    在OpenGL ES环境准备完成之后,会在GLThread中回调 onSurfaceCreated 方法。在这个方法中我们 需要准备我们需要绘制的图像。

    OpenGL纹理可以看成是能在OpenGL中使用的图像,在代码中我们通过id来操作纹理。

     @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //创建OpenGL 纹理 ,把摄像头的数据与这个纹理关联
            textures = new int[1];  //当做能在opengl用的一个图片的ID
            mCameraTexure.attachToGLContext(textures[0]);
            // 当摄像头数据有更新回调 onFrameAvailable
            mCameraTexure.setOnFrameAvailableListener(this);
    
            screenFilter = new ScreenFilter(cameraView.getContext());
        }
    
     @Override
        public void onFrameAvailable(SurfaceTexture surfaceTexture) {
            //  请求执行一次 onDrawFrame
            cameraView.requestRender();
        }
    

    让摄像头数据绑定到我们创建的纹理 textures[0] 之后,后续我们会将 textures[0] 交给OpenGL ES 的Sharder着色器去进行渲染,并且在渲染时也能去实现各种图像效果。

    OpenGL Sharder(着色器)需要使用GLSL(着色器语言)编写

    上述代码中的 mCameraTexure 就是摄像头预览数据绘制的窗口。 attachToGLContext 方法能够让摄像
    头预览的窗口与我们的纹理绑定起来。

    摄像头捕获

    由于我们本次会使用CameraX来完成摄像机的使用,使用CameraX,将捕获的图像与OpenGL纹理关联 起来也非常简单。再来回顾下我们之前直接通过CameraX与TextureView显示预览的代码。

     // 预览配置
        private Preview getPreView() {
            // 分辨率并不是最终的分辨率,CameraX会自动根据设备的支持情况,结合你的参数,设置一个最为接近的分辨率
            PreviewConfig previewConfig = new PreviewConfig.Builder()
                    .setTargetResolution(new Size(640, 480))
                    .setLensFacing(currentFacing) //前置或者后置摄像头
                    .build();
            Preview preview = new Preview(previewConfig);
            preview.setOnPreviewOutputUpdateListener(listener);
            return preview;
        }
    
    
    // 绑定声明周期
            CameraX.bindToLifecycle(lifecycleOwner, getPreView());
    
    @Override
        public void onUpdated(Preview.PreviewOutput output) {
            mCameraTexure = output.getSurfaceTexture();
    //        if (mCameraV.getSurfaceTexture() != surfaceTexture) {
    //            if (textureView.isAvailable()) {
    //                ViewGroup parent = (ViewGroup) displayer.getParent();
    //                parent.removeView(displayer);
    //                parent.addView(displayer, 0);
    //                parent.requestLayout(); }
    //                //设置布局中TextureView中的纹理画布完成预览
    //            textureView.setSurfaceTexture(mCameraTexure); }
    //        }
        }
    

    着色器

    着色器(Shader)是运行在GPU上的小程序。

    顶点着色器(vertex shader)
    如何处理顶点、法线等数据的小程序。

    #version 120
    attribute vec4 vPosition;//变量 float[4] 一个顶点 java 传过来
    attribute vec2 vCoord;//传给片元进行采样的纹理坐标
    varying vec2 aCoord;//易变变量 和片元写的一模一样 会传给片元
    void main() {
    //    gl_Position = vec4(vec3(0.0), 1.0);
        // 内置变量:把坐标点赋值给gl_position就ok
        gl_Position = vPosition;
        aCoord = vCoord;
    }
    
    

    这段程序就是一个顶点着色器的代码。与Java或者C一样,程序入口就是 main 方法。在 main 方法中只 要我们把要画的物体形状的点坐标给到 gl_position 即可。 vPosition 就是我们在CPU中确定的物体 形状坐标点。现在我们需要绘制的是摄像头采集图像,也就是矩形,那就需要传递4个顶点坐标数据到 着色器中。

    与我们熟悉的Java语法类似, vPosition 是变量名, vec4 是类型,而应用程序传递到顶点着色器中的 变量值需要使用 attribute 修饰。可以看成是 in 顶点着色器输入变量的修饰。

    vec4 其实就是vector向量,4表示这个向量包含4个数据。需要注意的是vec4只能存储float数据, 如果需要存储整型则需要使用ivec4。

    aCoord 定义为存储2个float的vec2类型,被varying修饰。如果说 attribute 是 in 输入变量的修饰, 那么 varying 就是 out 输出变量。我们会在片元着色器使用这个变量。

    这个顶点着色器中没有任何逻辑,只进行了赋值的操作,那么作为 attribute 输入的值 vPosition 与 vCoord 就需要我们在Android中传递进来。这两个值分别为顶点坐标与纹理坐标。

    我们需要根据OpenGL世界坐标系传递顶点坐标点来确定绘制的几何形状,需要在绘制的几何表面进行 贴图,就需要按照纹理坐标贴合几何顶点。顶点着色器执行4次, vPostion 接收的点坐标与 vCoord 分 别为:

    坐标.png
    -1.0,-1.0 与 0,0
     1.0,-1.0 与 1.0,0
     -1.0,1.0 与 0,1.0 
    1.0,1.0 与 1.0,1.0
    

    和U3D 坐标系类似

    片元着色器(fragment shader)
    如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一副图像的小程序。

    #version 120
    precision mediump float;//数据精度
    varying vec2 aCoord;
    // uniform 片元着色器必须用这个
    uniform samplerExternalOES vTexture;//samplerExternalOES 图片,采样器
    void main() {
    //    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        gl_FragColor = texure2D(vTexture,aCoord);//rgba
    }
    
    

    片元着色器中,使用内置函数 texture2D 采集对应坐标点的像素,赋值给 gl_FragColor 即可。

    #extension GL_OES_EGL_image_external : require :Android摄像头只能用samplerExternalOES 类型的纹理去接收摄像头的画面,而使用samplerExternalOES需要开启 GL_OES_EGL_image_external功能。

    uniform 变量是Android中需要传递给着色器的变量,不能被着色器修改。可以用于修饰共享变 量(在顶点和片元中声明方式完全一样)。

    aCoord 的定义需要和顶点着色器一模一样,但是在片元着色器能读取之前会通过光栅化传递, 光栅化程序在三角形的三个顶点之间进行插值处理,会访问三个顶点之间每一个像素,然后对每 个像素点执行片元着色器。所以在片元着色器中 aCoord 的值可以看成几何中每一个像素点的坐 标。

    texture2D(vTexture,aCoord) ,则是利用摄像头纹理的采样器 vTexture 获取 aCoord 坐标的 像素RGBA值并赋值给 gl_FragColor ,OpenGL就知道当前处理的片元是什么颜色从而绘制。

    实际上,在Android中我们就是通过OpenGL接口给着色器传参就好啦。我们可以在渲染接口的 onDraw 中调用OpenGL动态的给着色器传递参数。而各种绘制效果的处理在着色器中来完成。

    GLSL

    OpenGL着色语言(OpenGL Shading Language)

    attribute
    属性变量,顶点着色器输入数据。如:顶点坐标、纹理坐标、颜色等。

    uniforms
    一致变量。在着色器执行期间一致变量的值是不变的。类似常量,但不同的是,这个值在编译时期是未知的,由着色器外部初始化。

    varying
    易变变量,顶点着色器输出数据。是从顶点着色器传递到片元着色器的数据变量

    着色器使用

    着色器使用.png

    OpenGL 的结构流程

    结构.png

    相关文章

      网友评论

        本文标题:OpenGL (一)OpenGL ES 绘制基础

        本文链接:https://www.haomeiwen.com/subject/xbkjwktx.html