OpenGL-ES入门之介绍篇

OpenGL-ES入门之介绍篇

名词解释

OpenGL

通俗来讲,OpenGL是一个图形库,提供了一系列操作图形图像的API。因为OpenGL只是一个规范,具体的实现一般是由显卡提供。类似的图形库还有Windows平台的DirectX。

OpenGL-ES

OpenGL-ES是专为嵌入式设备准备的一个OpenGL的裁剪版(去除了一些复杂的图元)。OpenGL-ES支持Android、iOS等平台,并且它也是WebGL的基础。

EGL

EGL是OpenGL-ES与本地窗口系统之间的一个中间层API,一般由系统实现。EGL作为OpenGL-ES与设备之间的桥梁,将OpenGL-ES绘制在设备上。

WebGL

WebGL是OpenGL-ES在浏览器上的实现,WebGL1.0基于OpenGL-ES2.0实现,WebGL2.0基于OpenGl-ES3.0实现。

图形渲染管线

图形渲染管线也叫做渲染流水线,指的是将输入的原始图形数据经过渲染管线处理,输出一帧想要的图像的过程。

在OpenGL-ES中,任何事物都在3D空间中,而屏幕和窗口却都是2D的。所以图形渲染管线主要是在将输入的3D坐标转换成2D坐标,再讲2D坐标转换成实际有颜色的像素。(将输入的3D坐标画在3D坐标系中,然后根据视锥范围截取一个2D平面,将截取的平面转换为平面坐标显示)

2D坐标精确的表示一个点在2D空间的位置;而像素是这个点的近似值,像素的显示收到屏幕分辨率的限制

与CPU不同,GPU适合处理的问题都有这样的特征,一个问题可以分解为多个相同的小问题,每个问题之间相互独立不影响,所以GPU拥有大量的处理单元,处理单元数量越多,性能也就越好。而每个单元都可以并行处理,所以GPU的并行能力非常强。

可编程的渲染管线

自OpenGL-ES 2.0开始,采用的都是可编程的渲染管线,也就是开发者可以自定义部分流水线中的着色器,使得可以更细致的控制图形渲染管线。

着色器是运行在GPU上的小程序,这些小程序在图形渲染管线的某个特定部分运行,着色器是一个非常独立的程序,是一种把输入转化为输出的程序,着色器与着色器之间无法通信。

开发者可编程的阶段有两个,一个是顶点着色器,一个是片段着色器。

OpenGL-ES渲染管线

顶点着色器

着色器可以看成是一个纯函数,输入的所有顶点,都会依次执行该函数,在该函数中对输入的顶点进行处理,然后输出新的顶点位置。

图元装配

所谓图元就是几何形状,OpenGL-ES支持的图元有三种:点、线、三角形。而图元装配就是根据定义的图元类型将顶点着色器输出的顶点渲染成单独的图元,然后会图元进行裁剪、透视分割、视口变换。

裁剪:图元根据视锥体进行裁剪。根据视锥体的六个平面进行裁剪,超出视锥体之外的图元都将被裁剪抛弃(并不会真正裁剪,只是标记,x、y、z的绝对值大于1则表示不在视锥体内,会被裁剪)。裁剪后得到的坐标体系为(x,y,z,w),其中w用来在透视分割是处理远近效果。

透视投影-视锥体

正交投影-视锥体

透视分割:将裁剪后的坐标转换为规范化设备坐标。在该转换中会使用w坐标来进行处理,正交投影中w为1,而透视投影中w会根据远近值来生成,越远值越大。透视分割就是讲x、y、z坐标分别除以w,得到新的x、y、z坐标,由于坐标值都是小于w的,所以最终得到的值都是位于[-1,1]之间,这也就是所谓的规范化设备坐标。而对于透视投影来说,w的值越远越大,所以转换后,越远的坐标,转换后值越小,就会形成近大远小的效果。

视口变换:将设备坐标转换为窗口坐标,每个坐标都关联屏幕上的一个点。

光栅化

为装配后的图元生成对应的片段,每个片段代表着一个指定的像素位置。这些片段就是片段着色器中使用的片段。

片段着色器

定义每个像素应该输出什么颜色。在片段着色器中可以接收到顶点着色器输出的变量。

逐片段

片段着色器输出每个片段的颜色和深度,在最终加入到缓冲区前需要进过一系列测试和操作,这些操作称为逐片段。包括裁剪测试、模板测试、深度测试、混合、抖动等。最终通过的片段都会被加入到帧缓冲区中。

帧缓冲区

缓冲区是在EGL创建surface的时候生成的,缓冲区包含颜色缓冲区和深度缓冲区等。

GLSL

着色器编写语言,类C风格。

变量类型
1
2
3
4
5
6
7
8
9
OpenGL-ES 2.0 中变量类型为
uniform 可以在着色器中共享的变量,并且在着色器中只能读不能修改。
attribute 属性(数据可以放入到缓冲区中)顶点着色器中使用,类似in。
varying 顶点着色器和片段着色器中通信,在顶点着色器中的varying类似out,在片段着色器中的varying类似in。

OpenGL-ES 3.0 中变量类型
uniform
in
out
内建变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
顶点着色器:
- gl_VertexID: 输入变量,用于保存顶点的整数索引。
- gl_InstanceID: 输入变量,用于保存实例化绘图调用中图元的实例编号。
- gl_Position: 输出变量,输出顶点位置的裁剪坐标。该值在裁剪和视口阶段用于执行相应的图元裁剪以及从裁剪坐标到屏幕坐标的顶点位置转换。
- gl_PointSize: 输出变量,输出点精灵尺寸。
- gl_FrontFacing: 不能直接写入,根据顶点位置和图元类型生成。
- gl_DepthRange: 窗口坐标的深度范围。
该变量类型为struct gl_DepthRangeParameters{
highp float near;
highp float far;
highp float diff;
}

片段着色器:
- gl_FragCoord: 只读变量,保存片段的窗口相对坐标。
- gl_FrontFacing: 只读变量,片段是正面图元的一部分时为true,否则为false。
- gl_PointCoord: 只读变量,保存点精灵的纹理坐标,该坐标在点光栅化期间自动生成。
- gl_FragDepth:输出变量,用于覆盖片段的固定功能深度值。
- gl_FragColor:输出变量,输出该片段的颜色。(该变量在3.0中已删除,需要自行定义)

- texture2D(sampler2D, vec2 coord):从标准纹理中获取纹理数据。
- textureCube(samplerCube, vec3 coord:从立方体贴图纹理中获取纹理数据。
(3.0中上面两个函数被合成了一个函数)
- texture(sampler,coord): 内建函数,从纹理贴图中读取,sampler是绑定到纹理单元的采样器,制定纹理为读取来源。coord是从纹理贴图中读取的2d纹理坐标。该函数返回一个代表从纹理贴图中读取颜色的vec4,默认格式为RGBA。
精度限定符
1
2
3
4
顶点着色器有默认的精度,是highp,但是片段着色器没有默认的精度。
- highp
- mediump
- lowp

顶点着色器样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 3.0版本 需要特别声明版本信息
#version 300 es
// 定义两个属性,通过location指定该属性的索引值
layout(location = 0) in vec4 vPosition;
layout(location = 1) in vec4 aTextureCoord;
// 定义一个全局常量
uniform mat4 uTextureMatrix;
// 输出一个属性给片段着色器
out vec2 yuvTexCoords;
void main() {
// 输出顶点坐标
gl_Position = uTextureMatrix * vPosition;
yuvTexCoords = aTextureCoord;
}

// 2.0版本
attribute vec4 vPosition;
attribute vec2 aTextureCoord;
uniform mat4 uTextureMatrix;
varying vec2 yuvTexCoords;
void main() {
gl_Position = uTextureMatrix * vPosition;
yuvTexCoords = aTextureCoord;
}
片段着色器样例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 3.0版本
#version 300 es
#extension GL_OES_EGL_image_external_essl3 : require
precision mediump float;
uniform samplerExternalOES yuvTexSampler;
in vec2 yuvTexCoords;
out vec4 fragColor;
void main() {
fragColor = texture(yuvTexSampler,yuvTexCoords);
}

// 2.0版本
precision mediump float;
uniform sampler2D yuvTexSampler;
varying vec2 yuvTexCoords;

void main() {
gl_FragColor = texture2D(yuvTexSampler,yuvTexCoords);
}

EGL

通过Surface对象创建一个EGLSurface,将其连接到Surface的BufferQueue上,EGLSurface将会作为一个生产方,会将OpenGL-ES生产的缓冲区加入到该BufferQueue中,然后由该Surface的消费方进行处理。

Surface是一个生产方,EGLSurface也是一个独立的生产方。

一个Surface一次只能和一个EGLSurface进行连接,可以通过断开,然后重新连接另一个EGLSurface。

eglGetDisplay
1
2
3
4
5
6
// 获取当前设备中默认屏幕的handle,display对象可以认为就是一块物理的屏幕,如果没有对应的display,则返回EGL_NO_DISPLAY。

if ((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY) {
LOGE("witgao", "eglGetDisplay");
return;
}
eglInitialize
1
2
3
4
5
6
// 针对获取的屏幕对egl进行初始化(display,返回的主版本号,返回的次版本号)
EGLint major, minor;
if (!eglInitialize(display, &major, &minor) || major == NULL || minor == NULL) {
LOGE("witgao", "eglInitialize");
return;
}
eglChooseConfig

surface类型有三种,window_surface、pbuffer_surface、pixmap_surface,只有window_surface和window有关联,可以在window上显示。

绘制模式有两种,back buffer、single buffer,window_surface使用的是back buffer,back buffer是一块GPU中的buffer,可以通过EGL显示在屏幕上;而single buffer 是保存在内存中的位图,不支持显示在屏幕上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const EGLint attribs[] = {
// 设置surface类型
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
// 指定opengl-es版本为2.0
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_NONE
};
EGLConfig config;
EGLint numConfigs;

// 获取设备中支持指定属性的配置集合(display,指定的属性,输出的配置,输出配置的个数,所有支持指定属性的配置的个数)
if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs)) {
LOGE("witgao", "eglChooseConfig");
return;
}
eglCreateWindowSurface
1
2
3
4
5
6
7
8
// 创建一个提供给opengl-es绘制的surface(display,配置,原生window,指定属性)
if (!(eglSurface = eglCreateWindowSurface(display, config, aNativeWindow, 0))) {
LOGE("witgao", "eglCreateWindowSurface");
return;
}

// android中创建NativeWindow
ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(jenv, surface);
eglCreateContext
1
2
3
4
5
6
7
8
9
10
11
12
const EGLint context_attribs[] = {
// 设置context针对的opengl-es的版本,(EGL_NONE设置的是默认值,为1)
// 此处的版本需要和上面的EGL_RENDERABLE_TYPE 对应
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};

// 创建context,在context中保存了opengl-es的状态信息 (display,配置,共享context的handle 一般设为null,属性)
// 一个display可以创建多个context
if (!(context = eglCreateContext(display, config, 0, context_attribs))) {
LOGE("witgao", "eglCreateContext");
return;
}
eglMakeCurrent
1
2
3
4
5
6
7
8
9
// 将上面创建的context绑定到当前线程上,并将context与surface进行关联。当makeCurrent执行后,就可以调用opengl-es的api对context中的状态集进行设定,
// 然后进而向surface中绘制内容,再把surface中的内容读取出来。
// 一个线程中enable状态的context只能有一个,如果当前线程已经有了一个enable状态的context,那么会先执行其flush操作,将没有执行完成的命令全部执行完成,然后将其改为disable状态,将新传入的context改为enable状态。
// 如果想要释放当前的context,也就是将当前的context disable,那么将第二个和第三个参数设置为 EGL_NO_SURFACE,第四个参数设置为 EGL_NO_CONTEXT即可。
// context enable后,视口大小和裁剪大小都会被设置为surface的尺寸
if (!eglMakeCurrent(display, eglSurface, eglSurface, context)) {
LOGE("witgao", "eglMakeCurrent");
return;
}
eglSwapBuffers
1
2
3
4
5
// 当opengl-es将内容绘制完成,调用该方法将该缓冲区加入到Surface的BufferQueue中
if (!eglSwapBuffers(display, eglSurface)) {
LOGE("witgao", "eglSwapBuffers");
return;
}

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×