目录
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 基础
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 特效
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 转场
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES 函数
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES GPUImage 使用
零基础 OpenGL ES 学习路线推荐 : OpenGL ES 学习目录 >> OpenGL ES GLSL 编程
一.前言
在《OpenGL ES 名词解释一》中已经讲解了着色器渲染等相关知识,本篇文章着重讲解坐标系和矩阵相关内容;
二.坐标系
1.屏幕坐标系
屏幕坐标系 的 左下点(0, 1),右下角(1,1) , 左上角(0, 0) , 右上角(1 , 0)
2.纹理坐标系
纹理坐标系 的 左下点 (0, 0),右下角(1 , 0) , 左上角(0, 1 ), 右上角(1, 1)
3.顶点坐标系
顶点坐标系 的 左下点(-1, -1),右下角(1,-1) , 左上角(-1, 1) , 右上角(1 , 1)
4.图像坐标系
屏幕坐标系 的 左下点(0, 1),右下角(1,1) , 左上角(0, 0) , 右上角(1 , 0)
很多人有一个误解:认为 OpenGL ES 纹理原点在左上角,因为如果绘制时纹理坐标设在左下角,绘制的图像就是上下倒立;而纹理坐标设制在左上角显示正常;
原因:图像默认的原点在左上角,而 OpenGL ES 纹理读取数据或者 FBO 读取数据时都是以左下角开始,所以图像才会出现上下倒立的现象;
解决办法:
- 方案一:绘制时将纹理坐标上下镜像
- 方案二:绘制时将顶点坐标上下镜像
- 方案三:绘制时将图像上下镜像后在填充到 OpenGL ES 纹理
关于方案三:将图片上下颠倒可以使用 stb_image 完成
stbi_set_flip_vertically_on_load(true);//开起上下镜像
三.混合
假设一种不透明东西的颜色是 A,另一种透明的东西的颜色是 B ,那么透过 B 去看 A ,看上去的颜色 C 就是 B 和 A 的混合颜色,可以用这个式子来近似,设 B 物体的透明度为 alpha (取值为 0 – 1 ,0 为完全透明,1 为完全不透明)
R(C)=alpha*R(B)+(1-alpha)*R(A)
G(C)=alpha*G(B)+(1-alpha)*G(A)
B(C)=alpha*B(B)+(1-alpha)*B(A)
R(x)、G(x)、B(x)分别指颜色 x 的 RGB 分量。看起来这个东西这么简单,可是用它实现的效果绝对不简单,应用 alpha 混合技术,可以实现出最眩目的火光、烟雾、阴影、动态光源等等一切你可以想象的出来的半透明效果。
四.变换矩阵
1.平移
为向量(x,y,z)定义一个平移矩阵
2.旋转
旋转过程涉及到弧度与角度的转化:
弧度转角度
:角度 = 弧度 * (180.0f / PI)
角度转弧度
:弧度 = 角度 * (PI / 180.0f)
3.缩放
为向量(x,y,z)定义一个缩放矩阵
4.矩阵组合顺序
矩阵组合顺序 1:先平移,再旋转,最后缩放——— OK
矩阵组合顺序 2:先平移,再缩放,最后旋转——— ERROR
矩阵组合顺序 3:先缩放,再旋转,最后平移——— ERROR
(除了第一种,其他组合顺序都是错误的)
矩阵组合顺序可以参考 glm 官方 demo 案例:
#include <glm/vec3.hpp> // glm::vec3
#include <glm/vec4.hpp> // glm::vec4
#include <glm/mat4x4.hpp> // glm::mat4
#include <glm/ext/matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale
#include <glm/ext/matrix_clip_space.hpp> // glm::perspective
#include <glm/ext/scalar_constants.hpp> // glm::pi
glm::mat4 camera(float Translate, glm::vec2 const& Rotate)
{
glm::mat4 Projection = glm::perspective(glm::pi<float>() * 0.25f, 4.0f / 3.0f, 0.1f, 100.f);
glm::mat4 View = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, -Translate));
View = glm::rotate(View, Rotate.y, glm::vec3(-1.0f, 0.0f, 0.0f));
View = glm::rotate(View, Rotate.x, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 Model = glm::scale(glm::mat4(1.0f), glm::vec3(0.5f));
return Projection * View * Model;
}
至于矩阵组合顺序为什么是先平移,再旋转,最后缩放,后面将专门留一篇文章做详细讲解!可以关注学习目录《OpenGL ES 基础》
五.投影矩阵
由观察空间到裁剪空间在公式上左乘一个投影矩阵,投影矩阵的产生分为两种:正交投影和透视投影;
不管是正交投影还是透视投影,最终都是将视景体内的物体投影在近平面上,这也是 3D 坐标转换到 2D 坐标的关键一步。
正投影就是没有 3D 效果的投影方式,用于显示 2D 效果;
透视投影就是有 3D 效果的投影方式,用于显示 3D 效果.
1.正交投影
正交投影产生的效果无论你离物体多远多近,都不会产生近大远小的效果,大致如下图,视点作为观察点,视椎体由前后左右上下 6 个面包裹而成,物体在视椎体内部,最后投影到 near 近平面,视椎体范围之外将无法显示到屏幕上来
正投影就是没有 3D 效果的投影方式,用于显示 2D 效果;
透视投影就是有 3D 效果的投影方式,用于显示 3D 效果.
正交投影矩阵,由 Matrix.ortho 这个方法产生
void orthoM(float[] m, int mOffset,
float left, float right, float bottom, float top,
float near, float far)
可以把近平面看作屏幕,left、right、top、bottom 都是以近平面中心相对的距离,由于手机屏幕的长宽一般不相等,以短边为基准 1 ,长边取值为长/宽,所以如果一个竖屏的手机使用这个正交投影产生的矩阵应该是:
float ratio = (float)height / width;
Matrix.ortho(projectMatrix,0,-1, 1, -ratio, ratio, 1, 6);
2.透视投影
透视投影会产生近大远小的效果,正投影就是没有 3D 效果的投影方式,用于显示 2D 效果;透视投影就是有 3D 效果的投影方式,用于显示 3D 效果.产生的视椎体如下图:
透视投影也有响应的函数产生投影矩阵:
Matrix.frustumM(float[] m, int offset, float left,
float right, float bottom, float top,
float near, float far);
3.总结
经过上述的讲解,我们要完成 4 个空间转换,需要用到了 3 个转换矩阵:
从局部空间转换到世界空间,我们需要用到模型矩阵 ModeMatrix ,这个矩阵就是我们通常对物体进行 translate 、rorate 换后产生的矩阵
从世界空间到观察空间,我们需要用到观察矩阵 ViewMatrix ,这个矩阵可以 setLookAt 方法帮我们生成
从观察空间到裁剪空间,我们可以用到投影矩阵 ProjectMatrix,使用 ortho 、frustuM 还有 perspectiveM 方法产生投影矩阵
最后以上几个坐标依次左乘我们的定义的坐标 Position 就可以得到归一化坐标了
所以,总结出来的公式
//注意顺序
gl_Position = ProjectMatrix * ViewMatrix * ModeMatrix * g_Position ;
六.帧缓冲区帧
缓冲区就是显存,也被叫做帧缓存,它的作用是用来存储显卡芯片处理过或者即将提取的渲染数据。如同计算机的内存一样,显存是用来存储要处理的图形信息的部件。
最终”存活”下来的像素需要被显示到屏幕上,但是显示屏幕之前,这些像素是会被先提交在帧缓冲区的。帧缓存区的每一存储单元对应屏幕上的一个像素,整个帧缓存区对应一帧图像。
在下一个刷新频率到来时,视频控制器会把帧缓冲区内的内容映射到屏幕上。一般采用双缓冲机制,存在两个帧缓冲区。
七.VAO
VAO (顶点数组对象:Vertex Array Object)是指顶点数组对象,主要用于管理 VBO 或 EBO ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。
OpenGL 2.0 有 VBO,没有 VAO,VAO 是 OpenGL 3.0 才开始支持的,并且在 OpenGL 3.0 中,强制要求绑定一个 VAO 才能开始绘制。
八.VBO
VBO(顶点缓冲区对象:Vertex Buffer Object)是指把顶点数据保存在显存中,绘制时直接从显存中取数据,减少了数据传输的开销,因为顶点数据多了,就是坐标的数据多了很多的很多组,切换的时候很麻烦,就出现了这个 VAO,绑定对应的顶点数据
OpenGL 2.0 有 VBO,没有 VAO,VAO 是 OpenGL 3.0 才开始支持的,并且在 OpenGL 3.0 中,强制要求绑定一个 VAO 才能开始绘制。
九.PBO
**PBO (Pixel Buffer Object)是 OpenGL ES 3.0 的概念(OpenGL 2.0 不支持 PBO ,3.0 支持 PBO),称为像素缓冲区对象,**主要被用于异步像素传输操作。PBO 仅用于执行像素传输,不连接到纹理,且与 FBO (帧缓冲区对象)无关。PBO 设计的目的就是快速地向显卡传输数据,或者从显卡读取数据,我们可以使用它更加高效的读取屏幕数据。
- PBO 类似于 VBO(顶点缓冲区对象),PBO 开辟的也是 GPU 缓存,而存储的是图像数据。
- PBO 可以在 GPU 的缓存间快速传递像素数据,不影响 CPU 时钟周期,除此之外,PBO 还支持异步传输。
- PBO 类似于“以空间换时间”策略,在使用一个 PBO 的情况下,性能无法有效地提升,通常需要多个 PBO 交替配合使用。
十.FBO
FBO(Frame Buffer Object) 即帧缓冲对象。FBO 有什么作用呢?通常使用 OpenGL ES 经过顶点着色器、片元着色器处理之后就通过使用 OpenGL ES 使用的窗口系统提供的帧缓冲区,这样绘制的结果是显示到窗口(屏幕)上。
但是对于有些复杂的渲染处理,通过多个滤镜处理,这时中间流程的渲染采样的结果就不应该直接输出显示屏幕,而应该等所有处理完成之后再显示到窗口上。这个时候 FBO 就派上用场了。
FBO 是一个容器,自身不能用于渲染,需要与一些可渲染的缓冲区绑定在一起,像纹理或者渲染缓冲区。,它仅且提供了 3 个附着(Attachment),分别是颜色附着、深度附着和模板附着。
十一.UBO
**UBO,Uniform Buffer Object 顾名思义,就是一个装载 uniform 变量数据的缓冲区对象,**本质上跟 OpenGL ES 的其他缓冲区对象没有区别,创建方式也大致一致,都是显存上一块用于储存特定数据的区域。
当数据加载到 UBO ,那么这些数据将存储在 UBO 上,而不再交给着色器程序,所以它们不会占用着色器程序自身的 uniform 存储空间,UBO 是一种新的从内存到显存的数据传递方式,另外 UBO 一般需要与 uniform 块配合使用。
本例将 MVP 变换矩阵设置为一个 uniform 块,即我们后面创建的 UBO 中将保存 3 个矩阵。
#version 310 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
layout (std140) uniform MVPMatrix
{
mat4 projection;
mat4 view;
mat4 model;
};
out vec2 v_texCoord;
void main()
{
gl_Position = projection * view * model * a_position;
v_texCoord = a_texCoord;
}
设置 uniform 块的绑定点为 0 ,生成一个 UBO 。
GLuint uniformBlockIndex = glGetUniformBlockIndex(m_ProgramObj, "MVPMatrix");
glUniformBlockBinding(m_ProgramObj, uniformBlockIndex, 0);
glGenBuffers(1, &m_UboId);
glBindBuffer(GL_UNIFORM_BUFFER, m_UboId);
glBufferData(GL_UNIFORM_BUFFER, 3 * sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
绘制的时候更新 Uniform Buffer 的数据,更新三个矩阵的数据,注意偏移量。
glBindBuffer(GL_UNIFORM_BUFFER, m_UboId);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), &m_ProjectionMatrix[0][0]);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), &m_ViewMatrix[0][0]);
glBufferSubData(GL_UNIFORM_BUFFER, 2 *sizeof(glm::mat4), sizeof(glm::mat4), &m_ModelMatrix[0][0]);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
十二.TBO
纹理缓冲区对象,即 TBO(Texture Buffer Object),是 OpenGL ES 3.2 引入的概念,因此在使用时首先要检查 OpenGL ES 的版本,Android 方面需要保证 API >= 24 。
TBO 需要配合缓冲区纹理(Buffer Texture)一起使用,Buffer Texture 是一种一维纹理,其存储数据来自纹理缓冲区对象(TBO),用于允许着色器访问由缓冲区对象管理的大型内存表。
在 GLSL 中,只能使用 texelFetch 函数访问缓冲区纹理,缓冲区纹理的采样器类型为 samplerBuffer 。
生成一个 TBO 的方式跟 VBO 类似,只需要绑定到 GL_TEXTURE_BUFFER ,而生成缓冲区纹理的方式与普通的 2D 纹理一样。
//生成一个 Buffer Texture
glGenTextures(1, &m_TboTexId);
float *bigData = new float[BIG_DATA_SIZE];
for (int i = 0; i < BIG_DATA_SIZE; ++i) {
bigData[i] = i * 1.0f;
}
//生成一个 TBO ,并将一个大的数组上传至 TBO
glGenBuffers(1, &m_TboId);
glBindBuffer(GL_TEXTURE_BUFFER, m_TboId);
glBufferData(GL_TEXTURE_BUFFER, sizeof(float) * BIG_DATA_SIZE, bigData, GL_STATIC_DRAW);
delete [] bigData;
使用纹理缓冲区的片段着色器,需要引入扩展 texture buffer ,注意版本声明为 #version 320 es
#version 320 es
#extension GL_EXT_texture_buffer : require
in mediump vec2 v_texCoord;
layout(location = 0) out mediump vec4 outColor;
uniform mediump samplerBuffer u_buffer_tex;
uniform mediump sampler2D u_2d_texture;
uniform mediump int u_BufferSize;
void main()
{
mediump int index = int((v_texCoord.x +v_texCoord.y) /2.0 * float(u_BufferSize - 1));
mediump float value = texelFetch(u_buffer_tex, index).x;
mediump vec4 lightColor = vec4(vec3(vec2(value / float(u_BufferSize - 1)), 0.0), 1.0);
outColor = texture(u_2d_texture, v_texCoord) * lightColor;
}
绘制时如何使用缓冲区纹理和 TBO
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_BUFFER, m_TboTexId);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_TboId);
GLUtils::setInt(m_ProgramObj, "u_buffer_tex", 0);
十三.猜你喜欢
本文由博客 - 猿说编程 猿说编程 发布!