上一篇博客中,介绍了一些关于OpenGL ES2.0的一些概念,这篇博客,开始着手画个简单的图形——三角形。
我们绘制的三角形。肯定需要一个View去承载它显示,这个载体就是GLSurfaceView
An implementation of SurfaceView that uses the dedicated surface for displaying OpenGL rendering
GLSurfaceView从名字也可以看出,GL开头,继承于SurfaceView,一个专门用于OpenGL的SurfaceView。之所以用SurfaceView,因为他可以用自有的线程去维护页面的刷新,而不依赖于主线程。
GLSurfaceView增加了Renderer,用于承担渲染工作,它的使用方法如下:
glSurfaceView.setRenderer(new 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) {
}
});
首先我们先明确下我们绘制的步骤。OpenGL绘制过程,我们可以类比成我们用手机拍照。首先要拿出我们的手机打开摄像头;其次我们要知道要拍什么物体;然后我们调整我们自己的位置和手机方向,确保拍出来的照片足够好看;当我们按下快门的时候,摄像头把收集到的数据进行处理,转换成像素显示在手机上。有这样的思路,我们来捋顺下我们绘制OpenGL的过程:
了解了这些,我就就可以开始我们的代码了,首先,我们使用的是OpenGL ES2.0版本,需要在AndroidManifest.xml先添加说明:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
自定义自己的GLSurfaceView——PlaneGlSurfaceView
public class PlaneGlSurfaceView extends GLSurfaceView {
private OnTouchEventListener touchListener;
public PlaneGlSurfaceView(Context context) {
super(context);
init();
}
public PlaneGlSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setEGLContextClientVersion(2);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
this.setSceneWidthAndHeight(this.getMeasuredWidth(),
this.getMeasuredHeight());
}
public void setSceneWidthAndHeight(float mSceneWidth, float mSceneHeight) {
this.mSceneWidth = mSceneWidth;
this.mSceneHeight = mSceneHeight;
}
// 宽
private float mSceneWidth = 720;
// 高
private float mSceneHeight = 1280;
}
同样用于渲染Renderer我们也自定义一个——ShapeRenderer
public abstract class ShapeRenderer implements GLSurfaceView.Renderer{
/**
* 相乘矩阵
*/
protected final float[] mMVPMatrix = new float[16];
/**
* 透视投影矩阵
*/
protected final float[] mProjectionMatrix = new float[16];
/**
* 相机视图
*/
protected final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清屏 红,绿,蓝,透明度
GLES20.glClearColor(0.9f, 0.9f, 0.9f, 1f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在每次Surface尺寸变化时回调,例如当设备的屏幕方向发生改变时
//设置视图的尺寸,这就告诉了OpenGL可以用来渲染surface的大小
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
}
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;
}
其中GLES20.glClear方法为清除深度缓冲与颜色缓冲。
loadShader方法为创建着色器所用
可以看到此类为抽象类,同样基于后期拓展考虑,具体实现需继承此类。
无论什么图形,我们要显示它,肯定是要有顶点,颜色等信息。那我们可以把这些公共的东西提取出来,让不同的图形去继承它。图形类Shape
public abstract class Shape {
/**
* 顶点数据
*/
protected FloatBuffer vertexBuffer;
/**
* 顶点数据索引
*/
protected ShortBuffer indexBuffer;
/**
* 可执行程序
*/
protected int mProgram;
/**
* 坐标个数(xyz)
*/
protected static final int COORDS_PER_VERTEX = 3;
/**
* 顶点句柄
*/
protected int mPositionHandle;
/**
* 颜色句柄
*/
protected int mColorHandle;
public abstract void init();
public abstract void onDraw(float[] mMVPMatrix);
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;
}
init()方法初始化所用,onDraw(float[] mMVPMatrix)为绘制图形方法,参数为变换的矩阵。
这里也有个loadShader,作用和ShapeRenderer中是一模一样的 ,就是懒得删了。我们的目标是创建一个三角形,我们就去创建个三角形的类——Triangle
public class Triangle extends Shape{
/**
* 逆时针 x,y,z
*/
static float triangleCoords[] = {
// top
0.0f, 0.5f, 0.0f,
// bottom left
-0.5f, -0.5f, 0.0f,
// bottom right
0.5f, -0.5f, 0.0f
};
/**
* 颜色数组 红绿蓝 透明度
*/
float color[] = {0, 255, 0, 1.0f};
/**
* 顶点个数
*/
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
/**
* 每个顶点所占字节数
* (3*4 3是因为一个点由xyz组成 4是因为一个Float占用四字节)
*/
private final int vertexStride = COORDS_PER_VERTEX * 4;
@Override
public void init() {
// 初始化ByteBuffer,长度为arr数组的长度*4,因为一个float占4个字节
ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4);
// 数组排列用nativeOrder
bb.order(ByteOrder.nativeOrder());
// 从ByteBuffer创建一个浮点缓冲区
vertexBuffer = bb.asFloatBuffer();
// 将坐标添加到FloatBuffer
vertexBuffer.put(triangleCoords);
// 设置缓冲区从第一个开始读取
vertexBuffer.position(0);
}
/**
* 绘制
*/
@Override
public void onDraw(float[] mMVPMatrix) {
}
}
在我们初始化的时候,进行了一些列的类型转换。主要是因为Java的缓冲区数据存储结构为大端字节序(BigEdian),而OpenGl的数据为小端字节序(LittleEdian),因为数据存储结构的差异,所以,在Android中使用OpenGl的时候必须要进行下转换。
我们有了顶点颜色等基本数据,我们就需要让OpenGL ES去执行,我们在Triangle 类中创建专属于三角形自己的着色器
/**
* 顶点着色器
*/
private final String vertexShaderCode =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
"}";
/**
* 片元着色器
*/
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
vertexShaderCode 和fragmentShaderCode 分别为顶点着色器和片元着色器代码。想了解详情的可移步GLSL基础语法
有了着色器,我们就要让着色器可以跑起来,接下来我们初始化着色器,并创建可执行程序
@Override
public void init() {
...
// 顶点着色器和片元着色器加载
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 创建空的OpenGL ES程序
mProgram = GLES20.glCreateProgram();
// 添加顶点着色器到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 添加片段着色器到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 创建OpenGL ES程序可执行文件
GLES20.glLinkProgram(mProgram);
}
相机可以理解为我们用的手机。当用手机拍照时,我们所在的高度不同,远近不同,拿手机的姿势不一样,都会导致我们拍出来的照片不同,而拍出来的照片,就是相机视图。那我们看看如何设置相机视图
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上的分量
up向量上的分量,就相当于我们拿手机的姿势,一般情况下,我们都设置upY为1,这样就相当于我们手机是竖直向上的视角,拍出来的照片也是我们正常所看到的样子。
接下来是投影。上一篇文章介绍过,投影分为正交投影和透视投影,接下来看看代码去如何设置的。
正交投影投影物体的带下不会随观察点的远近而发生变化:
Matrix.orthoM (float[] m, //接收正交投影的变换矩阵
int mOffset, //变换矩阵的起始位置(偏移量)
float left, //相对观察点近面的左边距
float right, //相对观察点近面的右边距
float bottom, //相对观察点近面的下边距
float top, //相对观察点近面的上边距
float near, //相对观察点近面距离
float far) //相对观察点远面距离
透视投影:随观察点的距离变化而变化,观察点越远,视图越小,反之越大。
Matrix.frustumM (float[] m, //接收透视投影的变换矩阵
int mOffset, //变换矩阵的起始位置(偏移量)
float left, //相对观察点近面的左边距
float right, //相对观察点近面的右边距
float bottom, //相对观察点近面的下边距
float top, //相对观察点近面的上边距
float near, //相对观察点近面距离
float far) //相对观察点远面距离
代码上有一些参数理解起来可能有点费劲,这里解释下。先是left,right和bottom,top,这4个参数会影响图像左右和上下缩放比。所以往往会设置的值分别-(float) width / height和(float) width / height,top和bottom和top会影响上下缩放比,如果left和right已经设置好缩放,则bottom只需要设置为-1,top设置为1,这样就能保持图像不变形。也可以将left,right 与bottom,top交换比例,即bottom和top设置为 -height/width 和 height/width, left和right设置为-1和1。
那么就又延伸出一个问题,width / height为什么宽高还要有个比例呢。
从上一篇文章介绍的坐标系概念,OpenGL ES采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1。然而我们都知道,无论是我们手机还是平板,很少有正方形的屏幕。我们屏幕长宽实际上是不一样长的,但是我们的坐标系默认到边缘又是1。那么宽的1和高的1实际长度是不一样的,这里我们去缩放一下,让它看起来一样。借用个图片可能看起来更明显(图片借用他人博客)
我们把视线再拉回来,near和far参数又是什么意思呢。我们粗浅的理解下,就是一个立方体的前面和后面。near和far需要结合拍摄相机即观察者眼睛的位置来设置,例如setLookAtM(相机视图)中设置cx = 0, cy = 0, cz = 10,near设置的范围需要是小于10才可以看得到绘制的图像,如果大于10,图像就会处于了观察这眼睛的后面,这样绘制的图像就会消失在镜头前,far参数,far参数影响的是立体图形的背面,far一定比near大,一般会设置得比较大,如果设置的比较小,一旦3D图形尺寸很大,这时候由于far太小,这个投影矩阵没法容纳图形全部的背面,这样3D图形的背面会有部分隐藏掉的
这里突然又冒出来个新名词。那这个变换矩阵是做什么的呢,在之前我们有了相机视图,也有了投影视图。两者结合起来,才能让我们看到的东西更立体。承载视图的是矩阵,那么两个矩阵如何结合起来,那就是相乘。但是像我这种学渣级别的,离散数学、线性代数不挂科就万岁了。万幸,Android给我们做了这方面的工作,这就是变换矩阵。
Matrix.multiplyMM (float[] result, //接收相乘结果
int resultOffset, //接收矩阵的起始位置(偏移量)
float[] lhs, //左矩阵
int lhsOffset, //左矩阵的起始位置(偏移量)
float[] rhs, //右矩阵
int rhsOffset) //右矩阵的起始位置(偏移量)
介绍了这么多,那我们代码如怎么实现以上这些呢?
public class PlaneGlRenderer extends ShapeRenderer {
private Shape shape;
public void setShape(Shape shape) {
this.shape = shape;
}
@Override
public void onSurfaceCreated(GL10 gl10, javax.microedition.khronos.egl.EGLConfig eglConfig) {
super.onSurfaceCreated(gl10, eglConfig);
// 初始化图形
if (shape == null) {
shape = new Triangle();
}
shape.init();
}
@Override
public void onSurfaceChanged(GL10 gl10, int width, int height) {
super.onSurfaceChanged(gl10, width, height);
float ratio = (float) width / height;
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
// 这个投影矩阵被应用于对象坐标在onDrawFrame()方法中
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
}
...
}
而我们相乘得到的结果mMVPMatrix,就是我们图形具体绘制的时候用到的矩阵。
我们得到了我们想得到的数据,那么我们就要把数据传递给OpenGL程序去绘制三角形了。我们在Triangle中重写onDraw方法
@Override
public void onDraw(float[] mMVPMatrix) {
// 将程序添加到OpenGL ES环境
GLES20.glUseProgram(mProgram);
// 获取顶点着色器的位置的句柄
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用三角形顶点位置的句柄
GLES20.glEnableVertexAttribArray(mPositionHandle);
//准备三角形坐标数据
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 获取片段着色器的颜色的句柄
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
// 得到形状的变换矩阵的句柄
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// 将投影和视图转换传递给着色器
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
// 画三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
我们看到在获取顶点着色器句柄、片元着色器句柄和变换矩阵的句柄,传入参数有三个字符串。这三个字符串就是我们所写的顶点着色器和片元着色器代码中定义的变量名。
我们有了画三角形的方法,那么我们就需要在重绘的地方调用它。
public class PlaneGlRenderer extends ShapeRenderer {
...
@Override
public void onDrawFrame(GL10 gl10) {
super.onDrawFrame(gl10);
shape.onDraw(mMVPMatrix);
}
}
我们完成了上述代码,来看看我们的效果
看起来还不错。我们借用画一个三角形的契机,来初步了解OpenGL ES2.0如何去使用。在下一篇文章中,我们会介绍如何去画更复杂的图形Android OpenGL ES2.0从放弃到入门(三)——绘制正方形、圆形和立方体
关于OpenGL代码全部在一个项目中,托管在Github上——OpenGL4Android
欢迎转载,转载请保留文章出处。有梦想的咸鱼9527的博客https://blog.csdn.net/weixin_39560693/article/details/95485151