OpenGL学习2020-12-16
前言:
本篇仅为个人学习记录,是我看了一遍官方文档的总结,并非专业教程,它的第一作用是我在以后运用OpenGL的过程中能快捷地查看基础用法,即好记性不如翻文章,第二作用是能帮助完全不了解OpenGL的人快速入门,即用一个小时时间达到我看了一周官方文档的OpenGL的了解程度,之后进阶再看其他文章就更容易上手。
提示:
看代码是最好连同注释一起看了,方便理解,官方文档原版注释,我怕我翻译变味就不翻了。
一.在Manifest.xml中加入OpenGL清单声明
首先新建一个项目,在Manifest.xml中加入这句,用啥版本的就写啥版本的。
<!-- Tell the system this app requires OpenGL ES 2.0. -->
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<!-- <uses-feature android:glEsVersion="0x00030000" android:required="true" />-->
<!-- <uses-feature android:glEsVersion="0x00030001" android:required="true" />-->
版本适配 :
- 2.0 ——> Android 2.2(API 级别 8)开始可用
- 3.0 ——> Android 4.3(API 级别 18)开始可用
- 3.1 ——> Android 5.0(API 级别 21)开始可用
二.继承并实例化 GLSurfaceView 和GLSurfaceView.Renderer
自定义MyGLSurfaceView继承GLSurfaceView
import android.content.Context;
import android.opengl.GLSurfaceView;
public class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer renderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
renderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(renderer);
// Render the view only when there is a change in the drawing data
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
- setEGLContextClientVersion(version):通知默认的EGLContextFactory和默认的EGLConfigChooser,选择哪个EGLContext客户端版本。注意:1)必须用在 setRenderer() 之前;2)应该设置Manifest.xml里标明的版本。
- setRenderMode(renderMode):默认模式是‘持续渲染’
RENDERMODE_CONTINUOUSLY顾名思义就不停的绘制;‘指示渲染’ RENDERMODE_WHEN_DIRTY是只在surface被创建时或者调用 requestRender() 时才会绘制,可以省电,提高系统性能,能让gpu和cpu休息,好处多多,注意该方法要在 setRenderer() 之后调用。
自定义MyGLRenderer实现GLSurfaceView.Renderer
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView;
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {}
public void onDrawFrame(GL10 unused) {}
public void onSurfaceChanged(GL10 unused, int width, int height) {}
}
这里要明白这三个重写的方法的意义:
- onSurfaceCreated(GL10 unused, EGLConfig config):在SurfaceView创建时候调用一次,一般只调用一次,在这里面,我们一般用来做一些初始化操作,比如实例图形类;
- onDrawFrame(GL10 unused):这个简单,绘制,相当于 View 的 draw() ;
- onSurfaceChanged(GL10 unused, int width, int height):当 GLSurfaceView 的大小发生改变时会调用(例如屏幕翻转、代码改变View的大小),在最开始创建时会调用一次。
在MainActivity.class 中实例MyGLSurfaceView
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyGLSurfaceView mglsv = new MyGLSurfaceView(this);
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT);
addContentView(mglsv,lp);
}
}
这时,你的界面就是这样的:
三.(屏幕 - 视图)比例适配
首先来说说为什么要比例适配,一句话:因为OpenGL 假定屏幕采用均匀的方形坐标系,即它认为你的屏幕是正方形的,看图(左边未适配,右边已适配):
在MyGLRenderer创建投影和相机视图矩阵:
private final float[] vPMatrix= new float[16];
private final float[] projMatrix= new float[16];
private final float[] vMatrix= new float[16];
public void onSurfaceCreated(GL10 unused, EGLConfig config) {}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// create a projection matrix from device screen geometry
Matrix.frustumM(projMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
public void onDrawFrame(GL10 unused) {
...
// Create a camera view matrix
Matrix.setLookAtM(vMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Combine the projection and camera view matrices
Matrix.multiplyMM(vPMatrix, 0, projMatrix, 0, vMatrix, 0);
// Draw objects
...
}
-
glViewport(x, y, width, height) :设置画布的位置和宽高。x、y 以像素为单位,指定了画布的左下角位置。width、height 表示这个画布矩形的宽度和高度。
-
Matrix.multiplyMM(result, resultOffset, lhs, lhsOffset, rhs, rhsOffset):将两个矩阵合并(即相乘) 。result为目标矩阵(就是装合并之后的容器矩阵)、lhs和rhs是用来结合的两个矩阵。
-
glFrustumM(m, offset, left, Right, bottom, top, near, far):根据参数创建一个透视投影矩阵。除了near、far(视景体剪裁距离)其他的我也没懂怎么用,大家基本是固定的-ratio,ratio,-1,1。
-
Matrix.setLookAtM(rm, rmOffset, eyeX, eyeY, eyeZ, centerX, centerY,centerZ, upX, upY, upZ):设置相机位置,各个参数顾名思义就行了,最后三个参数表示(0,0,0)→(upX,upY,upZ)的方向为相机正上方的方向。
其他的就不解释了,注释写的很清楚了。
现在,你的GLSurfaceView就能以正确的比例显示了,不过你的界面还是这样:
四.使用 OpenGL ES 显示图形
1.绘制背景
自定义类实现 GLSurfaceview.Renderer
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
···
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
···
}
- glClearColor(red,green,blue,alpha) 和
glClear(mask):改变背景颜色。前者设置好清除颜色,后者利用前一个函数设置好的当前清除颜色设置窗口颜色。
2.创建图形类
三角形:
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
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
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 1.0f, 0f, 0f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
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 中,执行此操作的典型方式是为坐标定义浮点数的顶点数组。为了最大限度地提高工作效率,您可以将这些坐标写入 ByteBuffer 中,它会传递到 OpenGL ES 图形管道进行处理。(官方文档的原话,下面是其他文章找来的解释)
- OpenGL并不是对堆里面的数据进行操作,而是在直接内存中(Direct Memory),即操作的数据需要保存到NIO里面的Buffer对象中。而我们上面声明的float[]对象保存在堆中,因此,需要我们将float[]对象转为java.nio.Buffer对象。
- OpenGL在底层的实现是C语言,与Java默认的数据存储字节顺序可能不同,即大端小端问题。因此,为了保险起见,在将数据传递给OpenGL之前,我们需要指明使用本机的存储顺序,即bb.order(ByteOrder.nativeOrder())。
注意:形状的坐标是按照逆时针顺序定义的。绘制顺序非常重要,因为它定义了哪一边是形状的正面(您通常想要绘制的那一面),哪一边是背面(您可以使用 OpenGL ES 面剔除功能选择不绘制的那一面)。
正方形:
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float[] squareCoords = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
//绘制顺序:0,1,2形成一个三角形,0,2,3形成一个三角形
private short[] drawOrder = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (# of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (# of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
在 OpenGL 中定义三角形非常简单,如果希望定义复杂一点的图形比如方形,有多种方式可以执行此操作,但在 OpenGL ES 中绘制此类形状的典型方式是使用两个绘制在一起的三角形:
同样,对于表示该形状的两个三角形,您应按逆时针顺序定义顶点,并将这些值放入 ByteBuffer 中。为了避免两次定义这两个三角形共用的两个坐标,请使用绘制列表告知 OpenGL ES 图形管道如何绘制这些顶点。
完善三角形图形类:
public class Triangle {
···
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 vPMatrix;" +
"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;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
private final int mProgram;
private int positionHandle;
private int colorHandle;
private final int vertexCount = triangleCoords.length // COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public Triangle() {
···
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
public void draw(float[] vPMatrix) {
// 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);
// get handle to fragment shader's vPMatrix menber
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Apply the combined projection and camera view transformations
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(positionHandle);
}
}
上面,在自定义 Triangle 的构造方法中,完成了实例化program和shader并将它们关联。在 draw() 中为program各个组件设置数据并绘制。program和shader的解释如下:
顶点着色程序:用于渲染形状的顶点的 OpenGL ES 图形代码。
片段着色程序:用于使用颜色或纹理渲染形状面的 OpenGL ES 代码。
程序:包含您希望用于绘制一个或多个形状的着色程序的 OpenGL ES 对象。
您至少需要一个顶点着色程序绘制形状,以及一个 Fragment 着色程序为该形状着色。您还必须对这些着色程序进行编译,然后将其添加到之后用于绘制形状的 OpenGL ES 程序中(这些都是官方文档的原话)。
- GLES20.glGetUniformLocation(program, name):获取着色器程序中,指定为uniform类型变量的id。
- GLES20.glGetAttribLocation(program,name):获取着色器程序中,指定为attribute类型变量的id。
- GLES20.glDrawArrays(mode, first, count) 和GLES20.glDrawElements(mode,count,type,indices) :绘制。后者可以传入绘制顺序buffer,便于重复利用顶点。
如果你要绘制上面的方形,就要这样写
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, dlb);
MyGLRenderer 类中添加 loadShader() 方法,顾名思义就是加载着色器的静态方法:
public class MyGLRenderer implements GLSurfaceView.Renderer {
···
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (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后,直接在Renderer的onDrawFrame() 中调用triangle.draw() 就能显示图形了:
Triangle triangle;
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
···
triangle = new Triangle();
}
public void onDrawFrame(GL10 unused) {
...
// Draw objects
triangle.draw(vPMatrix);
}
五.添加动画、触摸反馈
1.添加动画
修改MyGLRenderer类:
private float[] rotationMatrix = new float[16];
@Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// Create a rotation transformation for the triangle
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(rotationMatrix, 0, angle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the vPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
上面代码看注释也很清楚了,在把‘投影×机视图(矩阵)’传递给三角形的渲染程序之前,先‘×旋转(矩阵)’。
Matrix.setRotateM() 等操作矩阵的方法请移步其他专业教程,看不看也行,顾名思义用起来就懂了。
现在你的程序是这样的:
2.响应触摸事件
为 MyGLSurfaceView 设置监听事件并设置渲染模式为‘指示渲染’以提高性能、添加触摸事件:
public class OneGlSurfaceView extends GLSurfaceView {
···
private final float TOUCH_SCALE_FACTOR = .5f;
private float previousX;
private float previousY;
···
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - previousX;
float dy = y - previousY;
// reverse direction of rotation above the mid-line
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();
}
previousX = x;
previousY = y;
return true;
}
在 MyGLRenderer 类中修改相应的方法:
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];
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(rotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the vPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, vPMatrix, 0, rotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
}
不多说直接上效果图:
好了,以上就是官方文档中的全部内容了。
六.展示2D图片
完善Square类
public class Square {
private static String vertexShaderCode = "uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 a_texCoord;" +
"varying vec2 v_texCoord;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
" v_texCoord = a_texCoord;" +
"}";
private static String fragmentShaderCode = "precision mediump float;" +
"varying vec2 v_texCoord;" +
"uniform sampler2D s_texture;" +
"void main() {" +
" gl_FragColor = texture2D(s_texture, v_texCoord);" +
"}";
//各种buffer
private FloatBuffer vertexBuffer;
private FloatBuffer textureBuffer;
private ShortBuffer drawListBuffer;
//表示数组中每两个数组成一个坐标
private static int COORDS_PER_TEXTURE = 2;
//纹理坐标,x和y都∈[0,1]
static float textureCoords[] = {
//画个纹理坐标系:
0f, 0f,//左上 (0,0)O·—— —— —— ——x(1,0)
0f, 1f,//左下 \ 纹理
1f, 1f,//右下 \ 图片
1f, 0f //右上 (1,1)\y ·(1,1)
};
Bitmap bitmap;
int mProgram, mPositionHandle, mTexCoordHandle, mMatrixHandle,mTexSamplerHandle;
public Square(Context context) {
···
//将float[]转换成FloatBuffer,同顶点坐标一样
textureBuffer = ByteBuffer.allocateDirect(textureCoords.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
textureBuffer.put(textureCoords);
textureBuffer.position(0);
//获取图片
bitmap = BitmapFactory.decodeResource(context.getResources(), R.raw.beauty);
//数据转换
int vertexShader = OneGlRenderer.loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode);
int fragmentShader = OneGlRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
// 创建空的OpenGL ES程序
mProgram = GLES20.glCreateProgram();
// 添加顶点着色器到程序中
GLES20.glAttachShader(mProgram, vertexShader);
// 添加片段着色器到程序中
GLES20.glAttachShader(mProgram, fragmentShader);
// 创建OpenGL ES程序可执行文件
GLES20.glLinkProgram(mProgram);
//用起来
GLES20.glUseProgram(mProgram);
//获取渲染程序中各个属性的hooker
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
mMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, "s_texture");
//顶点坐标数据传入到渲染程序对应的位置
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 12, vertexBuffer);
//纹理坐标数据传入到渲染程序对应的位置
GLES20.glEnableVertexAttribArray(mTexCoordHandle);
GLES20.glVertexAttribPointer(mTexCoordHandle, COORDS_PER_TEXTURE, GLES20.GL_FLOAT, false, 8, textureBuffer);
//创建并绑定纹理
int[] textures = new int[1];
GLES20.glGenTextures(textures.length, textures, 0);
int textureId = textures[0];
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
}
public void draw(float[] mvpMatrix) {
GLES20.glUniformMatrix4fv(mMatrixHandle, 1, false, mvpMatrix, 0);
GLES20.glUniform1i(mTexSamplerHandle, 0);
GLES20.glDrawElements(GLES20.GL_TRIANGLES,drawOrder.length,GLES20.GL_UNSIGNED_SHORT,drawListBuffer);
}
}
修改MyGLRenderer类:
Context context;
Square square;
public OneGlRenderer(Context context) {
this.context = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
···
square= new Square(context);
}
public void onDrawFrame(GL10 unused) {
...
// Draw objects
// triangle.draw(vPMatrix);
square.draw(vPMatrix);
}
现在你的应用就是这样的(图片是百度上下的,正方形):