OpenGL基础知识介绍
OpenGL简介
OpenGL (全写Open Graphics Library)是指定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图像(二维的亦可),是一个功能强大,调用方便的底层图形库。OpenGL在不同的平台上有不同的实现,但是它定义好了专业的程序接口,不同的平台都是遵照该接口来进行实现的,思想完全相同,方法名也是一致的,所以使用时也基本一致,只需要根据不同的语言环境稍有不同而已。OpenGL这套3D图形API从1992年发布的1.0版本到目前最新2014年发布的4.5版本,在众多平台上多有着广泛的使用
OpenGL 专业词解析
1.OpenGL上下文[context]
(1),在应用程序调用任何OpenGL指令之前,需要首先创建一个OpenGL的上下 文,这个上下文是一个非常庞大的状态机,保存了OpenGL中的各种状态,也是OpenGL指令执行的基础;
(2),由于OpanGL上下文是一个巨大的状态机,切换上下文往往会产生较大的开销,但是不同绘制模块可能需要使用完全独立的状态管理,因此,可以在应用程序中分别创建多个不同的上下文,在不同的线程中使用不同的上下文,上下文之间共享纹理,缓冲区等资源,这样的方案,会比反复切换上下文或者大量修改渲染状态更加合理高效
2.渲染
OpenGL渲染的原理是将输入的3d坐标的顶点数据结合纹理信息,和各种渲染状态绘制成屏幕上的2D像素片段的过程。如图
3.顶点数组和顶点缓冲区
画图一般是先画好骨架,然后再往骨架里面填充颜色,这对与OpenGL也是一样的,顶点数据就是要画的图像的骨架,和现实中不同的是:OpenGL中的图像都是由图元组成的,在OpenGL ES中,有3种类型的图元:点,线,三角形,在那些顶点数据最终存储在哪里呢?开发者可以选择设定的函数指针,再调用绘制方法的时候,直接由内存传入顶点数据,也就是说这部分数据之前是存储在内存当中,被称为顶点数组,而性能更高的做法是提前分配一块显存,将顶点数据预先传入当中,这部分显存,就被成为顶点缓冲区。顶点指的是我们在绘制一个图形时,它的顶点位置数据,而这个数据可以直接存储在数组中或者将其缓存到GPU内存中。
4.着色器程序Shader
OpenGL在处理shader时,和其他编译器一样,通过编译,链接等步骤,生成了着色器程序(glProgram),着色器程序同时包含顶点着色器和片段着色器的运算逻辑,在OpenGL进行绘制的时候,首先有顶点着色器对传入的顶点数据进行运算,再通过图元装配,将顶点装换为图元,然后进行光栅化,将图元这种矢量图形,转化为删格化数据,最后,将删格化数据传入片段着色器进行运算,片段着色器会对删格化数据中的每一个像素进行运算,并决定像素的颜色;
5.顶点着色器(VertexShader)
一般用来处理图形每个顶点变化【旋转、平移、投影等】;
顶点着色器是OpenGL中用于计算顶点属性的程序,顶点着色器是逐顶点运算的程序,也就是说每个顶点数据都会执行一次顶点着色器,当然这是并行的,并且顶点着色器运行中无法访问其他顶点的数据
一般来说典型的需要计算的顶点属性主要包括坐标变换,逐顶点光照运算等待,顶点坐标由自身坐标装换到归一化坐标系的运算,就是在这里发生的
6.几何着色器(GeometryShader)
在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。(爆炸效果)
7.片元/片段/像素着色器(FragmentShader)
一般用来处理图形中每个像素点颜色和填充;
片元着色器是OpenGL中用于计算片段(像素)颜色的程序,片段着色器逐像素运算的程序,也就是说每个像素都会执行一次片段着色器,且是并行执行
8.光栅化
是把顶点数据转换为片元的过程,具有将图转化为一个个栅格组成的图像的作用,特点是每个元素对应的缓冲区的一个像素;
光栅化就是把顶点数据转换为片元的过程,片元中的每个元素对应于帧缓冲区的一个像素;
光栅化其实是一种将几何图元变成二维图像的过程,该过程包含了两部分:
1.决定窗口坐标中的哪些整型删格化区域被基本图元占用,
2.分配一个颜色值和一个深度值到各个区域,光栅化的过程产生的是片元;
把物体的数学描述以及与物体相关的颜色信息转换为屏幕上用于对应位置的像素以及用于填充像素的颜色,这个过程称为光栅化,这是一个将模拟信号转化为离散信号的过程;
粗略地讲:你模型的那些顶点在经过各种矩阵变换后也仅仅是顶点。而由顶点构成的三角形要在屏幕上显示出来,除了需要三个顶点的信息以外,还需要确定构成这个三角形的所有像素的信息
9.纹理
为了能够把图片纹理(Bitmap)映射到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。纹理坐标用浮点数来表示,范围一般从0.0到1.0,左上角坐标为(0.0,0.0),右上角坐标为(1.0,0.0),左下角坐标为(0.0,1.0),右下角坐标为(1.0,1.0),如下图所示:
左图为纹理图和纹理坐标,右图为顶点图和顶点坐标。
将纹理映射到右边的两个三角形上(也就是一个矩形),需要将纹理坐标指定到正确的顶点上,才能使纹理正确的显示,否则显示出来的纹理会无法显示,或者出现旋转、翻转、错位等情况。
将右图顶点按照V2V1V4V3传入,以三角形条带方式绘制,则纹理坐标应按照V2V1V4V3传入。如果按照V3V4V1V2传入,会得到一个旋转了180度的纹理。如果按照V4V3V2V1传入,则会得到一个左右翻转的纹理
10.深度测试
(1)什么是深度?
深度其实就是该象素点在3d世界中距离摄象机的距离(绘制坐标),深度缓存中存储着每个象素点(绘制在屏幕上的)的深度值!
深度值(Z值)越大,则离摄像机越远。
深度值是存储在深度缓存里面的,我们用深度缓存的位数来衡量深度缓存的精度。深度缓存位数越高,则精确度越高,目前的显卡一般都可支持16位的Z Buffer,一些高级的显卡已经可以支持32位的Z Buffer,但一般用24位Z Buffer就已经足够了。
(2)为什么需要深度?
在不使用深度测试的时候,如果我们先绘制一个距离较近的物体,再绘制距离较远的物体,则距离远的物体因为后绘制,会把距离近的物体覆盖掉,这样的效果并不是我们所希望的。而有了深度缓冲以后,绘制物体的顺序就不那么重要了,都能按照远近(Z值)正常显示,这很关键。
实际上,只要存在深度缓冲区,无论是否启用深度测试,OpenGL在像素被绘制时都会尝试将深度数据写入到缓冲区内,除非调用了glDepthMask(GL_FALSE)来禁止写入。这些深度数据除了用于常规的测试外,还可以有一些有趣的用途,比如绘制阴影等等。
(3)启用深度测试
使用 glEnable(GL_DEPTH_TEST);
在默认情况是将需要绘制的新像素的z值与深度缓冲区中对应位置的z值进行比较,如果比深度缓存中的值小,那么用新像素的颜色值更新帧缓存中对应像素的颜色值。
但是可以使用glDepthFunc(func)来对这种默认测试方式进行修改。
其中参数func的值可以为GL_NEVER(没有处理)、GL_ALWAYS(处理所有)、GL_LESS(小于)、GL_LEQUAL(小于等于)、GL_EQUAL(等于)、GL_GEQUAL(大于等于)、GL_GREATER(大于)或GL_NOTEQUAL(不等于),其中默认值是GL_LESS。
一般来将,使用glDepthFunc(GL_LEQUAL);来表达一般物体之间的遮挡关系。
(4)启用了深度测试,那么这就不适用于同时绘制不透明物体。
11.混合
在测试阶段之后,如果像素依然没有被剔除,那么像素的颜色将会和帧缓冲区中颜色附着上颜色进行混合,混合的算法可以通过OpenGL的函数进行指定,但是OpenGL提供的混合算法有限,如果需要更加复杂的混合算法,一般可以通过像素着色器进行实现,当然性能会比原生的混合算法要差一些。混合是实现物体透明度的一种技术。就是说一个物体的颜色是本身的颜色和它背后其它物体的颜色的不同强度混合
12.GLSL
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。
GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
- 标量:标量表示的是只有大小没有方向的量,在GLSL中标量只有bool、int和float三种。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)对于标量的运算,我们最需要注意的是精度,防止溢出问题
- 向量:向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四位向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。
作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。
作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。
作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。 - 矩阵:在GLSL中矩阵拥有22、33、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值
- 采样器:采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤
- 结构体:和C语言中的结构体相同,用struct来定义结构体,关于结构体参考C语言中的结构体。
- 数组:数组知识也和C中相同,不同的是数组声明时可以不指定大小,但是建议在不必要的情况下,还是指定大小的好
- 空类型:空类型用void表示,仅用来声明不返回任何值得函数
限定符
attribute :一般用于各个顶点各不相同的量。如顶点颜色、坐标等。
uniform:一般用于对于3D物体中所有顶点都相同的量。比如统一颜色,统一变换矩阵等。
varying:表示易变量,一般用于顶点着色器传递到片元着色器的量。
const:常量。
浮点精度
与顶点着色器不同的是,在片元着色器中使用浮点型时,必须指定浮点类型的精度,否则编译会报错。精度有三种,分别为:
lowp:低精度。8位。
mediump:中精度。10位。
highp:高精度。16位。
内建变量
顶点着色器的内建变量
输入变量:
gl_Position:顶点坐标
gl_PointSize:点的大小,没有赋值则为默认值1,通常设置绘图为点绘制才有意义。
片元着色器的内建变量
输入变量:
gl_FragCoord:当前片元相对窗口位置所处的坐标。
gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。
输出变量
gl_FragColor:当前片元颜色
gl_FragData:vec4类型的数组。向其写入的信息,供渲染管线的后继过程使用。
13.相机
根据现实生活中的经历我们知道,对一个场景,随着相机的位置、拍摄出来的画面也是不相同。将相机对应于OpenGL的世界,决定相机拍摄的结果(也就是最后屏幕上展示的结果),包括相机位置、相机观察方向以及相机的UP方向。
相机位置:相机的位置是比较好理解的,就是相机在3D空间里面的坐标点。
相机观察方向:相机的观察方向,表示的是相机镜头的朝向,你可以朝前拍、朝后拍、也可以朝左朝右,或者其他的方向。
相机UP方向:相机的UP方向,可以理解为相机顶端指向的方向。比如你把相机斜着拿着,拍出来的照片就是斜着的,你倒着拿着,拍出来的就是倒着的。
Android 设置相机位置的方法:
Matrix.setLookAtM (float[] rm, //接收相机变换矩阵
int rmOffset, //变换矩阵的起始位置(偏移量)
float eyeX,float eyeY, float eyeZ, //相机位置
float centerX,float centerY,float centerZ, //观测点位置
float upX,float upY,float upZ) //up向量在xyz上的分量
14.投影
OpenGL 的世界是3D的,但是手机屏幕能够给我展示的终究是一个平面,只不过是在绘制的过程中利用色彩和线条让画面呈现出3D的效果。OpenGL 将这种从3D到2D的转换过程利用投影的方式使计算相对使用者来说变得简单可设置。
OpenGL 中有两种投影方式:即透视投影(perspective projection)和正交投影( orthographic projection)。
1.透视投影的投影线相交于一点,因此投影的结果与原物体的实际大小并不一致,而是会近大远小。因此透视投影更接近于真实世界的投影方式。
2.正交投影是平行投影的一种特殊情形,正交投影的投影线垂直于观察平面。平行投影的投影线相互平行,投影的结果与原物体的大小相等,因此广泛地应用于工程制图等方面。
Android 设置透视投影的方法:
Matrix.frustumM (float[] m, //接收透视投影的变换矩阵
int mOffset, //变换矩阵的起始位置(偏移量)
float left, //相对观察点近面的左边距
float right, //相对观察点近面的右边距
float bottom, //相对观察点近面的下边距
float top, //相对观察点近面的上边距
float near, //相对观察点近面距离
float far) //相对观察点远面距离
Android 设置正交投影的方法:
Matrix.orthoM (float[] m, //接收正交投影的变换矩阵
int mOffset, //变换矩阵的起始位置(偏移量)
float left, //相对观察点近面的左边距
float right, //相对观察点近面的右边距
float bottom, //相对观察点近面的下边距
float top, //相对观察点近面的上边距
float near, //相对观察点近面距离
float far) //相对观察点远面距离
Android 矩阵相乘的方法:
Matrix.multiplyMM (float[] result, //接收相乘结果
int resultOffset, //接收矩阵的起始位置(偏移量)
float[] lhs, //左矩阵
int lhsOffset, //左矩阵的起始位置(偏移量)
float[] rhs, //右矩阵
int rhsOffset) //右矩阵的起始位置(偏移量)
OpenGL 和 OpenGl ES 的区别
1.OpenGL ES和OpenGL 的关系
- OpenGL ES 是OpenGL 的子集,针对手机、PDA和游戏主机嵌入式设备而设计
- OpenGL ES 是从OpenGL 裁剪定制而来的,去除了glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性,剩下最核心有用的部分。可以理解成是一个在移动平台上能够支持OpenGL 最基本功能的精简规范。
3.OpenGL ES可以做什么
- 图片处理。比如图片色调转换、美颜等。
- 摄像头预览效果处理。比如美颜相机、恶搞相机等。
- 视频处理。摄像头预览效果处理可以,这个自然也不在话下了。
- 3D游戏。3D动画等.
4.OpenGL ES版本及Android支持情况
- OpenGL ES1.0是基于OpenGL 1.3的,OpenGL ES1.1是基于OpenGL 1.5的。Android 1.0和更高的版本支持这个API规范。OpenGL ES 1.x是针对固定硬件管线的。
- OpenGL ES2.0是基于OpenGL 2.0的,不兼容OpenGL ES 1.x。Android 2.2(API 8)和更高的版本支持这个API规范。OpenGL ES 2.x是针对可编程硬件管线的。
- OpenGL ES3.0的技术特性几乎完全来自OpenGL 3.x的,向下兼容OpenGL ES 2.x。Android 4.3(API 18)及更高的版本支持这个API规范。
- OpenGL ES3.1基本上可以属于OpenGL 4.x的子集,向下兼容OpenGL ES3.0/2.0。Android 5.0(API 21)和更高的版本支持这个API规范。
Android OpenGl ES 简单使用
点、线、三角形是OpenGL ES世界的图形基础。无论多么复杂的几何物体,在OpenGL ES的世界里都可以用三角形拼成
绘制一个简单的三角形
- 在AndroidManifest.xml文件中设置使用的OpenGL ES的版本:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
// 3.0的版本为0x00030000,3.1的版本为0x00030001。
- 创建一个GLSurfaceView用来显示图形,可以自定义继承GLSurfaceView 或者直接使用GLSurfaceView。设置OpenGL ES 2.0 版本和Render
public class OneGlSurfaceView extends GLSurfaceView {
private final OneGlRenderer oneGlRenderer;
public OneGlSurfaceView(Context context) {
super (context);
// 设置EGLContext客户端使用OpenGL ES 2.0 版本
setEGLContextClientVersion (2);
oneGlRenderer = new OneGlRenderer (context);
setRenderer (oneGlRenderer);
}
}
- 创建一个Render,可以自定义实现GLSurfaceView.Renderer接口。图形的具体渲染工作都是在Render中完成的。
public class OneGlRenderer1 implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 创建
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置大小和位置
}
@Override
public void onDrawFrame(GL10 gl) {
// 绘制
}
- 创建两个glsl文件,用GLSL语言写一个顶点着色器 和片元着色器程序
attribute vec4 vPosition;//定义一个4维向量 顶点位置
void main() {
gl_Position = vPosition; // gl_Position Shader的内置变量,分别为顶点位置
}
// attribute 变量 一般表示顶点数据,如:顶点坐标,法线,纹理坐标,顶点颜色等
// 只能在顶点着色器程序中使用
precision mediump float;//设置默认精度 中精度。10位。 精度 为float
uniform vec4 vColor;// 4维向量 顶点颜色
void main() {
gl_FragColor=vColor;
// gl_FragColor Shader的内置变量,片元颜色
// uniform 一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
}
- 确定顶点坐标和颜色,因为我们三角形只是一个平面图形,三角形正对我们来呈现。所以我们把三个顶点的Z坐标都设定为0。屏幕坐标系,从屏幕中心垂直到上下左右边缘距离都为1.0
private static float triangleCoords[] = {
0.0f, 0.5f, 0.0f, // 顶点
-0.5f, -0.5f, 0.0f, // 左下
0.5f, -0.5f, 0.0f // 右下
};
// R G B A
private float color[] = {0f, 0, 1f, 1.0f}; //蓝色
- Render接口有三个方法,分别为onSurfaceCreated、onSurfaceChanged和onDrawFrame。
在onSurfaceCreated方法中,我们来创建program对象,连接顶点和片元着色器,链接program对象
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//指定刷新颜色缓冲区时所用的颜色
//需要注意的是glClearColor只起到Set的作用,并不Clear。
//glClearColor更类似与初始化,如果不做,新的绘制就会绘制在以前的上面,类似于混合,而不是覆盖
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
triangle = new Triangle(mContext);
创建OpenGL ES 程序并链接
public Triangle(Context mContext) {
//申请底层空间
vertexBuffer = BufferUtil.floatBufferUtil (triangleCoords);
// 顶点着色器
int vertexShader = OneGlRenderer.loadShader (GLES20.GL_VERTEX_SHADER,
ShaderUtils.loadFromAssetsFile (vertexShaderCode,mContext.getResources ()));
// 片元着色器
int fragmentShader = OneGlRenderer.loadShader (GLES20.GL_FRAGMENT_SHADER,
ShaderUtils.loadFromAssetsFile (fragmentShaderCode,mContext.getResources ()));
// 创建空的OpenGL ES程序
mProgram = GLES20.glCreateProgram ();
// 添加顶点着色器到程序中
GLES20.glAttachShader (mProgram, vertexShader);
// 添加片段着色器到程序中
GLES20.glAttachShader (mProgram, fragmentShader);
// 创建OpenGL ES程序可执行文件
GLES20.glLinkProgram (mProgram);
}
编译着色器代码
public static int loadShader(int type, String shaderCode) {
// 创造顶点着色器类型(GLES20.GL_VERTEX_SHADER)
// 或者是片段着色器类型 (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 添加上面编写的着色器代码并编译它
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
申请空间的方法
public static FloatBuffer floatBufferUtil(float[] arr) {
FloatBuffer mBuffer;
// 初始化ByteBuffer,长度为arr数组的长度*4,因为一个float占4个字节
ByteBuffer qbb = ByteBuffer.allocateDirect(arr.length * 4);
// 数组排列用nativeOrder
qbb.order(ByteOrder.nativeOrder());
mBuffer = qbb.asFloatBuffer();
mBuffer.put(arr);
mBuffer.position(0);
return mBuffer;
}
在onSurfaceChanged中设置设置视图窗口:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// glViewport用于告诉OpenGL应把渲染之后的图形绘制在窗体的哪个部位、大小
GLES20.glViewport(0, 0, width, height);
最后在onDrawFrame中绘制:
public void draw() {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram (mProgram);
// 获取顶点着色器的位置的句柄
mPositionHandle = GLES20.glGetAttribLocation (mProgram, "vPosition");
// 启用三角形顶点位置的句柄
GLES20.glEnableVertexAttribArray (mPositionHandle);
//准备三角形坐标数据
GLES20.glVertexAttribPointer (mPositionHandle, 3,
GLES20.GL_FLOAT, false,
12, vertexBuffer);
// 获取片段着色器的颜色的句柄
mColorHandle = GLES20.glGetUniformLocation (mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv (mColorHandle, 1, color, 0);
// 绘制三角形
GLES20.glDrawArrays (GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray (mPositionHandle);
}
- 最终效果
- 绘制一个旋转动态的三角形(位置 和颜色可变)
修改一下着色器程序 如下:
顶点着色器
attribute vec4 vPosition;//定义一个4维向量 顶点位置
uniform mat4 uMVPMatrix;// 定义一个4x4 的变化矩阵
varying vec4 vColor;// 定义一个可变的 4维向量 传递颜色
attribute vec4 aColor;// 定义一个4维向量 赋值颜色
void main() {
gl_Position =uMVPMatrix* vPosition;// gl_Position Shader的内置变量,分别为顶点位置
vColor=aColor; // 颜色传递
}
片元着色器
precision mediump float;//中精度。10位。 精度 为float
varying vec4 vColor;// 可变的 4维向量 顶点颜色
void main() {
gl_FragColor=vColor;
// gl_FragColor Shader的内置变量,片元颜色
// uniform 一般用于对于3D物体中所有顶点都相同的量。比如光源位置,统一变换矩阵等。
}
-
使用变换矩阵
相机设置和投影设置并不是真正的设置,而是通过设置参数,得到一个使用相机后顶点坐标的变换矩阵,和投影下的顶点坐标变换矩阵,我们还需要把矩阵传入给顶点着色器,在顶点着色器中用传入的矩阵乘以坐标的向量,得到实际展示的坐标向量。注意,是矩阵乘以坐标向量,不是坐标向量乘以矩阵,矩阵乘法是不满足交换律的。
而通过上面的相机设置和投影设置,我们得到的是两个矩阵,为了方便,我们需要将相机矩阵和投影矩阵相乘,得到一个实际的变换矩阵,再传给顶点着色器 -
设置相机和投影,获取相机矩阵和投影矩阵,然后用相机矩阵与投影矩阵相乘,得到实际变换矩阵:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//计算宽高比
float ratio = (float) width / height;
// 设置透视投影 偏移量 近左 近右 近下 近上 近面距离 远面距离
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 20);
// 设置相机位置
Matrix.setLookAtM(mViewMatrix, 0, 5.0f, 5.0f, 10.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// 相乘计算变化矩阵
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
}
设置一个旋转矩阵
@Override
public void onDrawFrame(GL10 gl) {
// 创建一个旋转矩阵
float[] rotateMatrix = new float[16];
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
// 旋转矩阵
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
// 将旋转矩阵与投影和相机视图组合在一起
Matrix.multiplyMM(rotateMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 开始绘制
GraphicsTypeDraw(type,rotateMatrix);
}
绘制
glVertexAttribPointer 参数定义
/**
* index
*
* 指定要修改的通用顶点属性的索引。
*
* size
*
* 指定每个通用顶点属性的组件数。 必须为1,2,3或4.初始值为4。
*
* type
*
* 指定数组中每个组件的数据类型。 接受符号常量GL_BYTE,GL_UNSIGNED_BYTE,GL_SHORT,GL_UNSIGNED_SHORT,GL_FIXED或GL_FLOAT。 初始值为GL_FLOAT。
*
* normalized
*
* 指定在访问定点数据值时是应将其标准化(GL_TRUE)还是直接转换为定点值(GL_FALSE)。
*
* stride
*
* 指定连续通用顶点属性之间的字节偏移量。 如果stride为0,则通用顶点属性被理解为紧密打包在数组中的。 初始值为0。
*
* pointer
*
* 指定指向数组中第一个通用顶点属性的第一个组件的指针。 初始值为0。
*/
public void draw(float[] mvpMatrix) {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram);
// 得到形状的变换矩阵的句柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// 将投影和视图转换传递给着色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// 获取顶点着色器的位置的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用三角形顶点位置的句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
//准备三角形坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
//获取片元着色器的aColor成员的句柄
mColorHandle = GLES20.glGetAttribLocation(mProgram, "aColor");
//设置绘制三角形的颜色
GLES20.glEnableVertexAttribArray(mColorHandle);
GLES20.glVertexAttribPointer(mColorHandle, 4,
GLES20.GL_FLOAT, false,
0, colorBuffer);
// 画三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
- 展示效果
绘制正方形和正方体
- 构建正方形
在OpenGL的世界里面是没有正方形的,只有点、线、三角形。三角形就是OpenGLES提供的最复杂的图元单位。所以我们要绘制填充的正方形和圆形就需要利用三角形来实现。
正方形:正方形的构建比较简单,可以用两个三角形组成。当然,你也可以用很多很多三角形去合成一个正方形。如下图所示,我们可以按照123组成的三角形和134组成的三角形,两个拼合成一个正方形。
顶点数组
static float triangleCoords[] = {
-0.5f, 0.5f, 0.0f, // 1
-0.5f, -0.5f, 0.0f, // 2
0.5f, -0.5f, 0.0f, // 3
0.5f, 0.5f, 0.0f // 4
};
- 图形的绘制
GLES20.glDrawArrays的第一个参数表示绘制方式,第二个参数表示偏移量,第三个参数表示顶点个数。
绘制方式有:
int GL_LINES //将传入的坐标作为单独线条绘制,ABCD 4个顶点,绘制AB、CD、2条线
int GL_LINE_STRIP //将传入的顶点作为折线绘制,ABCD四个顶点,绘制AB、BC、CD三条线
int GL_LINE_LOOP //将传入的顶点作为闭合折线绘制,ABCD四个顶点,绘制AB、BC、CD、DA四条线。
int GL_TRIANGLES //将传入的顶点作为单独的三角形绘制,ABCDAC绘制ABC,DAC两个三角形
int GL_TRIANGLE_FAN //将传入的顶点作为扇面绘制,ABCD绘制ABC、ACD、2个三角形
int GL_TRIANGLE_STRIP //将传入的顶点作为三角条带绘制,ABCD绘制ABC,BCD,2个三角形
如图用顶点法传不同的绘制方式结果:
GL_TRIANGLE_STRIP
GL_TRIANGLE_STRIP的方式绘制连续的三角形,比直接用GL_TRIANGLES的方式绘制三角形少好多个顶点,效率会高很多。另外,GL_TRIANGLE_STRIP并不是只能绘制连续的三角形构成的物体,我们只需要将不需要重复绘制的点重复两次即可。比如,传入ABCDEEFFGH坐标,就会得到ABC、BCD、CDE以及FGH四个三角形
GL_TRIANGLE_FAN
扇面绘制是以第一个为零点进行绘制,通常我们绘制圆形,圆锥的锥面都会使用到,值得注意的是,最后一个点的左边应当与第二个点重合,在计算的时候,起点角度为0度,终点角度应包含360度。
- 顶点法和索引法
上述提到的绘制,使用的都是GLES20.glDrawArrays,也就是顶点法,是根据传入的定点顺序进行绘制的。还有一个方法进行绘制GLES20.glDrawElements,称之为索引法,是根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元进行绘制。
顶点法拥有的绘制方式,索引法也都有。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以相对顶点法减少很多重复顶点占用的空间。
索引法区别的地方:
// 索引内存buffer
private ShortBuffer indexBuffer;
// 此数组中每个顶点的坐标数
private static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = {
-0.5f, 0.5f, 0.0f, // 1 索引 0
-0.5f, -0.5f, 0.0f, // 2 索引 1
0.5f, -0.5f, 0.0f, // 3 索引 2
0.5f, 0.5f, 0.0f , // 4 索引 3
};
static short index[]={
0,1,2,0,2,3
};
// 初始化索引buff
indexBuffer = BufferUtil.shortBufferUtil (index);
//索引法绘制正方形
// glDrawElements 参数定义
// 1 指定要渲染的图元类型。 接受符号常量GL_POINTS,GL_LINE_STRIP,GL_LINE_LOOP,GL_LINES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN和GL_TRIANGLES。
// 2 指定要渲染的元素数。
// 3 指定indices中值的类型。 必须是GL_UNSIGNED_BYTE或GL_UNSIGNED_SHORT。
// 4 指定指向存储索引的位置的指针。
GLES20.glDrawElements(GLES20.GL_TRIANGLES,index.length, GLES20.GL_UNSIGNED_SHORT,indexBuffer);
-
展示效果
-
绘制正方体
立方体是是由六个正方形组成,我们将这六个正方形绘制出来,正方体就绘制出来了。
首先确定顶点,正方体有8个顶点,我们列出8个顶点,并用索引绘制发绘制。
如图:
// 8个顶点
static float cubeCoords[] = {
-1.0f,1.0f,1.0f, //正面左上0
-1.0f,-1.0f,1.0f, //正面左下1
1.0f,-1.0f,1.0f, //正面右下2
1.0f,1.0f,1.0f, //正面右上3
-1.0f,1.0f,-1.0f, //反面左上4
-1.0f,-1.0f,-1.0f, //反面左下5
1.0f,-1.0f,-1.0f, //反面右下6
1.0f,1.0f,-1.0f, //反面右上7
};
// 6个面
static short cubeIndex[]={
0,3,2,0,2,1, //正面
0,1,5,0,5,4, //左面
0,7,3,0,4,7, //上面
6,7,4,6,4,5, //后面
6,3,7,6,2,3, //右面
6,5,1,6,1,2 //下面
};
// 8个顶点颜色
private float cubeColor[] = {
0f,0f,1f,1f,
0f,0f,1f,1f,
0f,1f,0f,1f,
0f,1f,0f,1f,
1f,0f,0f,1f,
1f,0f,0f,1f,
1f,0f,1f,1f,
1f,1f,0f,1f,
};
- 注意创建程序前需要开启深度测试并在绘制前清除深度缓存,否则绘制的效果不正常
如图:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//开启深度测试
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
@Override
public void onDrawFrame(GL10 gl) {
// 清除深度缓存
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Obj,STL格式3D模型加载
- 模型加载:
3D模型文件有多种格式的,每个格式的解析有所区别。文件解析得到法向量数据源,定点数据源等。
//解析二进制的Stl文件
public Model parserBinStl(InputStream in) throws IOException {
if (stlLoadListener != null)
stlLoadListener.onstart();
Model model = new Model();
//前面80字节是文件头,用于存贮文件名;
in.skip(80);
//紧接着用 4 个字节的整数来描述模型的三角面片个数
byte[] bytes = new byte[4];
in.read(bytes);// 读取三角面片个数
int facetCount = BufferUtil.byte4ToInt(bytes, 0);
model.setFacetCount(facetCount);
if (facetCount == 0) {
in.close();
return model;
}
// 每个三角面片占用固定的50个字节
byte[] facetBytes = new byte[50 * facetCount];
// 将所有的三角面片读取到字节数组
in.read(facetBytes);
//数据读取完毕后,可以把输入流关闭
in.close();
parseModel(model, facetBytes);
if (stlLoadListener != null)
stlLoadListener.onFinished();
return model;
}
- 模型渲染
模型的渲染,和之前绘制各种形体也差不多了,为了让3D模型呈现出立体效果,示例中,增加了简的光照
//开启光照
public void openLight(GL10 gl) {
gl.glEnable(GL10.GL_LIGHTING);
gl.glEnable(GL10.GL_LIGHT0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, BufferUtil.floatBufferUtil(ambient));
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, BufferUtil.floatBufferUtil(diffuse));
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, BufferUtil.floatBufferUtil(specular));
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, BufferUtil.floatBufferUtil(lightPosition));
}
public void enableMaterial(GL10 gl) {
//材料对环境光的反射情况
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT, BufferUtil.floatBufferUtil(materialAmb));
//散射光的反射情况
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE, BufferUtil.floatBufferUtil(materialDiff));
//镜面光的反射情况
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR, BufferUtil.floatBufferUtil(materialSpec));
}
- 传入从STL文件读取的值,然后和渲染一个立方体一样,渲染出模型就OK了
- 展示效果如图: