本文正在参加「金石计划」
前篇回顾*
前面一篇文章我们介绍了关于如何在OpenGL中使用纹理,以及纹理坐标,纹理映射等内容,相信你们已经都学会了。那么今天我们来做个稍微难点的东西:使用OpenGL画一个立方体贴图。就是下面这种效果。
废话不多说,我们直接进入正题。
纹理坐标设置
同样的,我们需要先定义好立方体的顶点坐标:一个立方体有6个面,一个面需要2个三角形图元组成,也就是需要6个顶点(当然你可以使用EBO索引缓冲对象来让两个三角形共用4个顶点),这里为了方便大家理解,我就直接使用一个面6个顶点来处理了,6个面就是36个顶点坐标,分别定义如下:
//0.初始化顶点数据
float vertices[] = {
//顶点位置坐标------------纹理坐标
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
注意:每一行的前三位为一个顶点坐标位置,后两位为纹理坐标位置,因为我们还需要为每个面都设置一个纹理贴图。
设置好纹理坐标,下一步就是对于的顶点着色器以及片段着色器的编写了。
顶点着色器:
#version 300 es
layout(location = 0) in vec4 vPosition;
layout(location = 1) in vec2 texCords;
out vec2 TexCoord;
void main()
{
gl_Position = vPosition;
TexCoord = texCords;
}
片段着色器:
#version 300 es
precision mediump float;
in vec2 TexCoord;
out vec4 fragColor;
uniform sampler2D textureColor;
void main()
{
vec3 picColor = vec3 (texture(textureColor,TexCoord));
fragColor = vec4 (picColor, 1.0 );
}
可以看到着色器代码部分很简单,和前面一篇文章的设置正方形的顶点着色器基本一样。所以不要觉得GLSL是什么很玄乎的东西,你只要把他看做是运行在GPU上的一段处理输入并输出的代码就可以。
in和out关键字的使用,一个是输入,一个是输出。
编译着色器:
programObj = GLUtils::CreateProgram(vShaderStr,fShaderStr);
编译着色器一般步骤:
- 1.创建顶点着色器对象,给顶点着色器对象绑定着色器代码,编译顶点着色器。
- 2.用同样的方法,创建并编译片段着色器、
- 3.创建着色器程序对象,并将1,2中创建的着色器对象依附到着色器程序对象中,最后链接着色器。
这里我将编译着色器封装在了一个工具库中,详见github。
设置顶点属性:
//2.生成VAO,VBO对象,并绑定顶点属性
GLuint VBO;
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
//顶点坐标属性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)0);
glEnableVertexAttribArray(0);
//顶点颜色属性
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(GL_NONE);
由于这里我们没有使用EBO索引缓冲对象,所以不需要EBO来设置顶点索引。
顶点属性的标准流程:
- 1.创建顶点数组对象VAO以及顶点缓冲对象VBO
- 2.绑定顶点数组对象VAO
- 3.绑定VBO,并为VAO绑定对应的顶点数据
- 4.设置顶点坐标属性/纹理坐标属性等
- 5.解绑VAO对象。
记住,这是标准流程,不同形状需求,无非就是改动顶点数据结构,其实都可以套用这个步骤。
加载纹理贴图:
//生成纹理
glGenTextures(1, &textureID);
ImageUtil::_stbi_set_flip_vertically_on_load(true);
int width, height, nrComponents;
unsigned char *data = ImageUtil::_stb_image_load("/sdcard/mmpic.png", &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
ImageUtil::_stb_image_free(data);
}
else
{
LOGCATE("Texture failed to load at path: %s",data);
ImageUtil::_stb_image_free(data);
}
纹理加载过程也是标准化过程:
- 1.调用glBindTexture绑定纹理,由于这里使用的是2D纹理,所以使用的是GL_TEXTURE_2D
- 2.调用glTexImage2D为绑定的2D纹理绑定添加纹理数据。
- 3.调用glGenerateMipmap,自动生成纹理mipmap。
这里加载纹理贴图使用到了stb_image.h这个类库,强烈推荐大家可以去github上看看,这个库中还有很多其他好用的东西。
stb_image.h:github.com/nothings/st…
设置好这些,我们使用下面方式来渲染:
glDrawArrays(GL_TRIANGLES, 0, 36);
如果一切顺利你可能看到如下效果:
额,不是说好的立方体么,这不只是个长方形么。。
先别急,还没完工,为了可以看到立方体的效果,我们还需要来了解下OpenGL中的坐标系统。
坐标系统
首先要知道OpenGL的所有坐标转换都是为了将local坐标转换为屏幕上的坐标。
这个转换过程需要经过下面几个坐标:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间( Screen Space)
而这些坐标在转换过程中,又需要使用到几个转换矩阵,其中最重要的几个分别是模型(Model) 、视图(View) 、投影(Projection) 三个矩阵。
首先,顶点坐标开始于局部空间(Local Space) ,称为局部坐标(Local Coordinate) ,然后经过世界坐标(World Coordinate) ,观察坐标(View Coordinate) ,裁剪坐标(Clip Coordinate) ,并最后以屏幕坐标(Screen Coordinate) 结束。下面的图示显示了整个流程及各个转换过程做了什么。
- 局部坐标是对象相对于局部原点的坐标;也是对象开始的坐标。
- 将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统。这些坐标是相对于世界的原点的。
- 接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标。
- 在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上。
- 最后,我们需要将裁剪坐标转换为屏幕坐标,我们将这一过程成为视口变换(Viewport Transform) 。视口变换将位于-1.0到1.0范围的坐标转换到由
glViewport
函数所定义的坐标范围内。最后转换的坐标将会送到光栅器,由光栅器将其转化为片段。
上面这段是引用的官网话语。要去看的话需要花很多时间而且并不一定能看懂,下面小余就用大白话讲下:
- 1.渲染的物体进行不是有可能进行平移,旋转,缩放等动作么,那么就用模型(Model)矩阵来处理。
- 2.物体是渲染好了,但是我们从哪个角度观察呢?就是我们眼睛看物体哪个地方,那个方向呢,那就用视图(View)矩阵来处理。
- 3.在我们真实世界中,看到的物体是不是越远就越小,越近就越大呢?实现这种效果就用到投影(Projection)矩阵来处理。
这样是不是很好理解了呢?
根据上面的讲解,我们先来设置这几个矩阵,让我们看到立方体效果。
首先来改造下顶点着色器代码:
#version 300 es
layout(location = 0) in vec4 vPosition;
layout(location = 1) in vec2 texCords;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection*view*model*vPosition;
TexCoord = texCords;
}
我们使用uniform关键字,传递三个全局的mat4矩阵变量,分别表示model矩阵,view矩阵,以及projection矩阵。
在C++代码中对顶点着色器中的全局变量进行设置:
glm::mat4 model,view,projection;
glUniformMatrix4fv(glGetUniformLocation(programObj,"model"),1,GL_FALSE,glm::value_ptr(model));
view = glm::lookAt(glm::vec3(0.0f,0.0f,2.5f),glm::vec3(0.0f,0.0f,0.0f),glm::vec3(0.0f,1.0f,0.0f)); glUniformMatrix4fv(glGetUniformLocation(programObj,"view"),1,GL_FALSE,glm::value_ptr(view));
projection = glm::perspective(45.0f,(GLfloat)mScreenWidth/(GLfloat)mScreenHeight,0.1f,100.0f);
glUniformMatrix4fv(glGetUniformLocation(programObj,"projection"),1,GL_FALSE,glm::value_ptr(projection));
对Model矩阵,我们没有做任何处理,只是设置了一个初始值。
对View矩阵,我们使用了glm::lookAt方法进行设置,下面来看lookAt方法参数:
- 参数1:代表从哪看,就是观察者眼睛所在的位置,比如我们要在渲染的3D物体的左边看,那么就可以设置为vec3(-1.0f,0.0f,0.0f),就好像我们在手机的侧边-1.0f距离处看这个物体,这个距离可以调节,越大,眼睛位置离物体就越远,物体看起来就越小。
- 参数2:代表眼睛看的位置,这里我们指定为vec3(0.0f,0.0f,0.0f),表示眼睛注视物体的中心位置,可调节。
- 参数3:这是一个垂直我们眼睛的一个分量,这个向量是用来干嘛的呢,就是标识我们是头是斜着看呢还是正对着,还是倒立看呢?能理解不。
通过上面的三个参数,我们就建立了一个观察者,观察者的不同,我们看到的图像也不同,就好像:
一个女人漂亮不漂亮,光看背影是不够的。。
对projection矩阵,有两种,一种是正射投影,一种是透视投影,正射投影用的不多,我们来说下透视投影。
透视投影
透视投影是为了解决真实世界中的,离我们越远的物体,看起来就会越小。用火车路来看吧:
透视投影很好的解决了这个问题,物体远近体现在我们的屏幕中就是物体的深度,Z轴。
透视投影使用glm::perspective来设置。
- 参数1:定义了fov的值,它表示的是视野(Field of View) ,并且设置了观察空间的大小。对于一个真实的观察效果,它的值经常设置为45.0,当然大家也可以自定义大小来看效果。
- 参数2:设置了宽高比,由视口的高除以宽.
- 参数3和4:设置了平截头体的近和远平面。我们经常设置近距离为0.1而远距离设为100.0。所有在近平面和远平面的顶点且处于平截头体内的顶点都会被渲染。
glm::perspective
所做的其实就是再次创建了一个定义了可视空间的大的平截头体,任何在这个平截头体的对象最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被可视化为一个不均匀形状的盒子,在这个盒子内部的每个坐标都会被映射到裁剪空间的点。一张透视平截头体的照片如下所示:
好了,设置好上面三个矩阵之后,我们来看下效果:
嗯,图像变为了正方形了,但是还是没看到立方体呀?
如果你观察仔细,前面我们把View矩阵的第一个参数也就是眼睛的位置设置为了vec3(0.0f,0.0f,2.5f),x和y为0,z值为一个正数,z值代表深度,说明我们眼睛位置是正对屏幕的。所以你看到的还是一个正方形,我们把眼睛位置改为vec3(2.5f,1.5f,2.5f),看下效果:
效果已经出来了,下面我们让这个正方体随着屏幕滑动起来。
float lastX = 0,lastY = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float dx = 0,dy = 0;
float curX = event.getX();
float curY = event.getY();
switch (action){
case MotionEvent.ACTION_DOWN:
lastX = curX;
lastY = curY;
break;
case MotionEvent.ACTION_MOVE:
dx = curX-lastX;
dy = curY-lastY;
mRenderer.move(dx,dy);
lastX = curX;
lastY = curY;
break;
case MotionEvent.ACTION_UP:
mRenderer.move(0.0f,0.0f);
break;
}
Log.d("MyGLSurfaceView","curX:"+curX+" curY:"+curY);
Log.d("MyGLSurfaceView","dx:"+dx+" dy:"+dy);
return true;
}
这里我们定义一个native接口,将每次屏幕滑动触发的x和y偏移量传递给native层。
在native层我们使用model矩阵来处理滑动效果:
/**
* 计算旋转轴
* 使用两个向量的叉乘可以获取一个垂直这两个向量的新向量
* 向量V为移动的方向向量vec3(moveX,moveY,0.0f);
* 向量K为垂直屏幕的向量vec3(0.0f,0.0f,1.0f); 可以使用单位向量代替
* */
if(moveX!=0||moveY!=0){
float radius = 360.0f;
float moveL = sqrt(moveX*moveX+moveY*moveY);
LOGCATD("move moveL:%f",moveL);
glm::vec3 _v = glm::vec3(moveX,moveY,0.0f);
glm::vec3 _k = glm::vec3(0.0f,0.0f,1.0f);
glm::vec3 _u = glm::cross(_k,_v);
float angle = sin(moveL)*radius;
LOGCATD("move angle:%f %f %f",_u.x,_u.y,_u.z);
model = glm::rotate(model,angle,_u);
}else {
model = glm::rotate(model,0.0f,glm::vec3(0.0f,1.0f,0.0f));
}
来看最终效果:
好了,本文就讲解到这里了,本文主要通过一个立方体的渲染过程,来讲解了OpenGL中的坐标系统的使用。
下篇文章,将会讲解OpenGL中的光照使用。我是小余,关注我我们下期见。
参考: