美文网首页 移动 前端 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