欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵
本篇文章不讲opengl技术,重点放在Android端如何接入opengl。代码参考Android官方文档实现。完整代码已上传到github
如果你是opengl零基础,可以先了解opengl,或者读完本篇文章后,抽时间补充。
实现效果
基于touch事件,转动三角形,通过实现这个小demo,来完整了解Android端opengl的基础开发
构建OpenGL ES环境
简化开发流程,我们基于GLSurfaceView来开发,并使用Android/Java层的OpenGL ES API,下一篇会讲述在native层开发opengl。OpenGL版本选择2.0,你也可以选择3.0或更高版本,不影响基础开发的学习。
AndroidManifest.xml增加声明
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果是GL3.0则声明为
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
增加GLSurfaceView
GLSurfaceView继承自SurfaceView,实现内部接口GLSurfaceView.Renderer,以控制实际的渲染逻辑,而GLSurfaceView则提供GL环境,如此大大简化了opengl的环境配置。
一个Activity来组织所有逻辑
//Activity里添加GLSurfaceView
public class OpenGLActivity extends AppCompatActivity {
private MyGLSurfaceView glView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
glView = new MyGLSurfaceView(this);
setContentView(glView);
}
}
实现MyGLSurfaceView
import android.content.Context;
import android.opengl.GLSurfaceView;
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer renderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
// EGL是链接opengl和底层硬件显示的桥梁
setEGLContextClientVersion(2);
renderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(renderer);
// 设置渲染模式,两种场景各有优点
// WHEN_DIRTH 有新的数据才出发绘制,对性能友好
// CONTINUOUSLY 按一定频率持续刷新,不断的触发绘制
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
设置Renderer,方便实现demo逻辑,写成MyGlSurfaceView的内部静态类
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// 设置opengl的清屏颜色
// (0.0f, 0.0f, 0.0f, 1.0f)表示RGBA4通道,这里指黑色
// (1.0f, 0.0f, 0.0f, 1.0f)则表示红色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// 调用gl的清屏操作,每次渲染之前,最好清除屏幕上一次的缓存颜色
// 清屏颜色onSurfaceCreated中设置为黑色,可以设置成任意颜色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
// 设置窗口属性(0, 0)表示原点坐标
// width height表示显示的窗口大小
GLES20.glViewport(0, 0, width, height);
}
}
EGL简介
EGL,全称:embedded Graphic Interface,是 OpenGL ES 和底层 Native 平台 视窗系统之间的接口。EGL和OpenGL一样,也是一个通用标准,各个平台、硬件的实现不同。iphone上不支持EGL,用的是EAGL。
OpenGL ES 是负责 GPU 工作的,目的是通过 GPU 计算,得到一张图片,这张图片在内存中其实就是一块 buffer,存储有每个点的颜色信息等。而这张图片最终是要显示到屏幕上,所以还需要具体的窗口系统来操作,OpenGL ES 并没有相关的函数。所以,OpenGL ES 有一个好搭档 EGL。
(www.cnblogs.com/yongdaimi/p… "OpenGL ES 与 EGL、GLSL的关系")
二、绘制三角形
先定义一个三角形
如果你的opengl是零基础,可能会有点吃力,建议后面补上相关知识。
public class Triangle {
private FloatBuffer vertexBuffer;
// 定义每个顶点三个坐标,即x、y、z分量
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = {
// in counterclockwise order:
// 三角形的顶点按逆时针顺序定义,用于检测前后,此处可以先不用管
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// 三角形顶点颜色
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 初始化顶点数据的buffer
ByteBuffer bb = ByteBuffer.allocateDirect(
// 每个坐标用4字节存储
triangleCoords.length * 4);
// 采用设备硬件的本地字节序
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
注意:opengl es底层调用的是C/C++,数据结构的定义不同,不能直接用java层的api来定义,或涉及到数据转换、效率较低。
FloatBuffer说明
(stackoverflow.com/questions/1… "why FloatBuffer")
四边形的定义读者可参考官方文档 (developer.android.google.cn/training/gr… "定义形状")
原理相同,区别是四边形要定义两个三角形。
绘制三角形
三角形顶点属性一般不会改变,onSurfaceCreated中创建一次即可
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
// 定义顶点着色器,暂时点不做任何处理
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
// 片段着色器,暂时啥也没干
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// initialize a triangle
mTriangle = new Triangle();
}
...
}
加载glsl shader的方法,定义在Renderer中
// 加载完,返回一个句柄,后面要用到
public static int loadShader(int type, String shaderCode) {
// create a vertex shader type(GLES20.GL_VERTEX_SHADER)
// or a fragment shader tye(GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
回到Triangle中,实现shader的加载、编译和链接,增加draw方法
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
// 加载顶点着色器
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
// 加载片元着色器
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// 创建一个空的OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// 添加vertexShder
GLES20.glAttachShader(mProgram, vertexShader);
// 添加fragmentShader
GLES20.glAttachShader(mProgram, fragmentShader);
// 创建一个可执行的program,每一个shader编译后都是一个program
GLES20.glLinkProgram(mProgram);
}
// 添加draw方法,都是标准化的opengl的操作,不做赘述
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(positionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(positionHandle);
}
}
注意 attribute、uniform、varying的区别,新手容易弄混,参考
(sites.google.com/site/pavelk… "attribute uniform varying区别")
万事俱备,只欠东风,GLSurfaceView调用triangle.draw()。
public void onDrawFrame(GL10 unused) {
...
triangle.draw();
}
实现效果
有个问题
定义的是等边三角形,为什么渲染出来变形了呢? 原因是,手机宽高比不等于1,一般opengl计算空间是1 * 1 * 1的单位立方体,生成的一帧图像放到手机屏幕上就被拉升变长了。
下一步,我们就来处理屏幕的适配。
三、view/投影变换,适宜屏幕大小
如果你对mvp变换不了解,或者不认识上面这张图,建议先熟悉下,参考learnOpenGL教程
修改Triangle.class
// 修改顶点着色器,增加mvp矩阵
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int vPMatrixHandle;
// 修改draw方法,增加顶点变换矩阵 mvpMatrix
public void draw(float[] mvpMatrix) {
...
// get handle to shape's transformation matrix
vPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(positionHandle);
}
顺便回顾下glUniformMatrix4fv:
void glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat * value)
通过一致变量(uniform修饰的变量)引用将一致变量值传入渲染管线。
location : uniform的位置。
count : 需要加载数据的数组元素的数量或者需要修改的矩阵的数量。
transpose : 指明矩阵是列优先(column major)矩阵(GL_FALSE)还是行优先(row major)矩阵(GL_TRUE)。
value : 指向由count个元素的数组的指针。
修改GLSurfaceView#MyGLRenderer
增加投影矩阵,projection matrix的设置,保证opengl的视窗和手机window的视窗大小一致,如此缩放后也是等比缩放,不会变形。
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
// 参数说明
// 0 projectionMatrix矩阵偏移
// -ratio ratio -1 1 3 7分别表示左、右、上、下、近平面、远平面
// 用ratio表示视窗的左、右,即视窗按比例被压瘪了
Matrix.frustumM(projectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
projection 矩阵忘了的,看下图回顾下投影矩阵定义的视锥体,完全不知道的可以跳过:
生成最终的顶点变换矩阵vPMatrix
@Override
public void onDrawFrame(GL10 unused) {
...
// 设置相机位置,即view变换矩阵
// 参数说明,opengl mvp很基础的概念,此处再详细说明下
// 0 viewMatrix 偏移
// (0 0 3)相机位置,模拟人眼的视角。文档里是-3,表示从后面看三角形,比较别扭,影响旋转方向
// 我这里改成+3,从正面看三角形
// (0 0 0)相机看的方向,(0 0 0)表示“人眼”注视原点
// (0 1 0)相机上方与Y轴对齐,表示人脑袋没有左右偏移,为正视方向
Matrix.setLookAtM(viewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(vPMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
// Draw shape
triangle.draw(vPMatrix);
}
效果:
四、动画与触摸交互
三角形自动旋转
有了上面的基础,做一个三角形自动旋转就很简单了,直接看代码及注释
Renderer里修改代码:
private float[] rotationMatrix = new float[16];
Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// 创建旋转矩阵,根据time来改变三角形旋转的角度,达到动态效果
// 一周360°,每次更新0.09°,则需要4000mm转一圈,即周期是4秒
// 注意,opengl里很多场景下使用弧度制,即π
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
// (0 0 -1.0f)表示围绕-z方向旋转,根据右手定则,为顺时针(大拇指指向-z方向,朝屏幕里)
Matrix.setRotateM(rotationMatrix, 0, angle, 0, 0, -1.0f);
// 投影矩阵与旋转矩阵相乘,得到最终的矩阵
Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
最后修改GLSurfaceView里的渲染模式
public MyGLSurfaceView(Context context) extends GLSurfaceView {
...
// 什么都不设置
// 或设置为CONTINUOUSLY,自动更新
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
实现效果
手势控制旋转
so easy,reading the fucking code!!!
GLSurfaceView中响应touch事件,动态修改mvp矩阵,控制旋转
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float previousX;
private float previousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
// 手势移动的x y距离作为旋转的角度
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - previousX;
float dy = y - previousY;
// reverse direction of rotation above the mid-line
// 算法很巧妙,y > height/2时,手势点在下半屏幕,x方向的距离要反过来
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
renderer.setAngle(
renderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
// 请求渲染
requestRender();
}
// 记录previous坐标
previousX = x;
previousY = y;
return true;
}
更改GLSurfaceView的渲染模式
public MyGLSurfaceView(Context context) {
...
// 改成主动渲染,有数据变化时,才通知渲染,性能更好
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
Renderer增加angle的设置,将旋转矩阵中angle替换为手动设置的值,上面自动旋转的angle是随时间动态计算的。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
Matrix.setRotateM(rotationMatrix, 0, mAngle, 0, 0, -1.0f);
Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
实现效果
欢迎关注公众号:sumsmile /专注图像处理的移动开发老兵
参考资料
[1] android GL ES官方文档: developer.android.google.cn/guide/topic…
[2] github demo: github.com/summer-go/A…
[3] opengl教程: learnopengl-cn.github.io/
[4] OpenGL ES 与 EGL、GLSL的关系: www.cnblogs.com/yongdaimi/p…
[5] why FloatBuffer: stackoverflow.com/questions/1…
[6] 定义形状: developer.android.google.cn/training/gr…
[7] attribute uniform varying区别: sites.google.com/site/pavelk…
[8] 坐标系统: learnopengl-cn.github.io/01%20Gettin…
[9] 右手坐标系参考图: www.jianshu.com/p/e25bf6dc1…