问:学OpenGL能干嘛? 答: 为所欲为。
说起
OpenGLES
,大家可能都敬而远之,其实它并没有想象中的那么可怕
,当然也并没有那么容易
都0202年了,本系列使用OpenGLES3.0
,这是一次有预谋的计划:
1.黑屏的实现
1.1 GLSurfaceView的使用
Android中OpenGL通过GLSurfaceView进行展现,实现Renderer接口
实现接口方法:onSurfaceCreated
、onSurfaceChanged
、onDrawFrame
public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
private static final String TAG = "GLWorld";
public GLWorld(Context context) {
this(context,null);
}
public GLWorld(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
setRenderer(this);//设置渲染器
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.e(TAG, "onSurfaceCreated: " );
GLES30.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);//rgba
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.e(TAG, "onSurfaceChanged: " );
GLES30.glViewport(0, 0, width, height);//设置GL视口
}
@Override
public void onDrawFrame(GL10 gl) {
Log.e(TAG, "onDrawFrame: " );
}
}
日志如下: 默认onDrawFrame约隔16ms会不断刷新
2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onSurfaceCreated:
2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onSurfaceChanged:
2020-01-10 13:34:35.368 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame:
2020-01-10 13:34:35.440 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame:
2020-01-10 13:34:35.458 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame:
2020-01-10 13:34:35.461 3934-4035/com.toly1994.tolygl E/GLWorld: onDrawFrame:
1.2 GLWorld的使用
它就相当于一个View,现在直接放到setContentView中,便可以显示
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(GLWorld(this))
}
}
2.从点线开始说起
现在你对OpenGLES的认识就像眼前的黑屏一样,一无所知
我们需要去点亮它,展现出一个新世界,大门即将打开,请扶好站稳。
这道光,你扎住了,就已经赢了一大半,抓不住,就…算了吧
2.1 GLPoint的编写
为了不混乱和方便使用,创建一个GLPoint类负责点的绘制测试
[1] 准备顶点着色代码和片段着色代码
[2] 准备顶点和颜色数据
[3] 加载着色器代码并初始化程序
[4] 绘制逻辑 (添加程序->启用顶点->绘制)
public class GLPoint {
//顶点着色代码
final String vsh = "#version 300 es\n" +
"layout (location = 0) in vec3 aPosition; \n" +
"layout (location = 1) in vec4 aColor;\n" +
"\n" +
"out vec4 color2frag;\n" +
"\n" +
"void main(){\n" +
" gl_Position = vec4(aPosition.x,aPosition.y, aPosition.z, 1.0);\n" +
" color2frag = aColor;\n" +
"gl_PointSize=10.0;"+
"}";
//片段着色代码
final String fsh = "#version 300 es\n" +
"precision mediump float;\n" +
"out vec4 outColor;\n" +
"in vec4 color2frag;\n" +
"\n" +
"void main(){\n" +
" outColor = color2frag;\n" +
"}";
//顶点数组
private final float vertexes[] = { //以逆时针顺序
0.0f, 0.0f, 0.0f,//原点
};
// 颜色数组
private final float colors[] = new float[]{
1.0f, 1.0f, 1.0f, 1.0f,//白色
};
private int program;
private static final int VERTEX_DIMENSION = 3;
private static final int COLOR_DIMENSION = 4;
private FloatBuffer vertBuffer;
private FloatBuffer colorBuffer;
private int aPosition =0;//位置的句柄
private int aColor =1;//颜色的句柄
public GLPoint() {
program = initProgram();
vertBuffer= GLBuffer.getFloatBuffer(vertexes);
colorBuffer= GLBuffer.getFloatBuffer(colors);
}
private int initProgram() {
int program;
////顶点shader代码加载
int vertexShader = GLLoader.loadShader(GLES30.GL_VERTEX_SHADER, vsh);
//片段shader代码加载
int fragmentShader = GLLoader.loadShader(GLES30.GL_FRAGMENT_SHADER, fsh);
program = GLES30.glCreateProgram();//创建空的OpenGL ES 程序
GLES30.glAttachShader(program, vertexShader);//加入顶点着色器
GLES30.glAttachShader(program, fragmentShader);//加入片元着色器
GLES30.glLinkProgram(program);//创建可执行的OpenGL ES项目
return program;
}
public void draw(){
//清除颜色缓存和深度缓存
GLES30.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
// 将程序添加到OpenGL ES环境中
GLES30.glUseProgram(program);
//启用顶点句柄
GLES30.glEnableVertexAttribArray(aPosition);
//启用颜色句柄
GLES30.glEnableVertexAttribArray(aColor);
//准备坐标数据
GLES30.glVertexAttribPointer(
aPosition, VERTEX_DIMENSION,
GLES30.GL_FLOAT, false,
VERTEX_DIMENSION * 4, vertBuffer);
//准备颜色数据
GLES30.glVertexAttribPointer(
aColor, COLOR_DIMENSION,
GLES30.GL_FLOAT, false,
COLOR_DIMENSION * 4, colorBuffer);
//绘制点
GLES30.glDrawArrays(GLES30.GL_POINTS, 0, vertexes.length / VERTEX_DIMENSION);
//禁用顶点数组
GLES30.glDisableVertexAttribArray(aPosition);
GLES30.glDisableVertexAttribArray(aColor);
}
}
2.2 缓冲数据
float数组需要通过FloatBuffer进行缓冲,以提高效率
/**
* float数组缓冲数据
*
* @param vertexs 顶点
* @return 获取浮点形缓冲数据
*/
public static FloatBuffer getFloatBuffer(float[] vertexs) {
FloatBuffer buffer;
///每个浮点数:坐标个数* 4字节
ByteBuffer qbb = ByteBuffer.allocateDirect(vertexs.length * 4);
//使用本机硬件设备的字节顺序
qbb.order(ByteOrder.nativeOrder());
// 从字节缓冲区创建浮点缓冲区
buffer = qbb.asFloatBuffer();
// 将坐标添加到FloatBuffer
buffer.put(vertexs);
//设置缓冲区以读取第一个坐标
buffer.position(0);
return buffer;
}
2.3 使用
在我们的GLWorld中创建GLPoint对象,在onDrawFrame中绘制即可
public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
private static final String TAG = "GLWorld";
private GLPoint glPoint;
public GLWorld(Context context) {
this(context,null);
}
public GLWorld(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
setRenderer(this);//设置渲染器
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
glPoint = new GLPoint();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);//设置GL视口
}
@Override
public void onDrawFrame(GL10 gl) {
glPoint.draw();
}
}
2.4 多点绘制
现在绘制点,通过更改顶点和颜色即可
//顶点数组
private final float vertexes[] = { //以逆时针顺序
0.0f, 0.0f,0.0f,//原点
0.5f,0.0f,0.0f,
0.5f,0.5f,0.0f,
0.0f,0.5f,0.0f,
};
// 颜色数组
private final float colors[] = new float[]{
1.0f, 1.0f, 1.0f, 1.0f,//白色
1.0f, 0.0f, 0.0f, 1.0f,//红色
0.0f, 1.0f, 0.0f, 1.0f,//绿色
0.0f, 0.0f, 1.0f, 1.0f,//蓝色
};
2.5 画线
前面处理好了,划线比较简单
//绘制
GLES30.glLineWidth(10);
//GLES30.glDrawArrays(GLES30.GL_POINTS, 0, vertexes.length / VERTEX_DIMENSION);
//GLES30.glDrawArrays(GLES30.GL_LINES, 0, vertexes.length / VERTEX_DIMENSION);
//GLES30.glDrawArrays(GLES30.GL_LINE_LOOP, 0, vertexes.length / VERTEX_DIMENSION);
GLES30.glDrawArrays(GLES30.GL_LINE_STRIP, 0, vertexes.length / VERTEX_DIMENSION);
注:着色器的代码会有单独一篇进行讲解,此处暂不做解释。
3.线绘制与比例校正
可能你已经发现了,本应是个正方形,可现实成了矩形
原因在于视口的比例不对,现在是这样个坐标系:
3.1 GLLine添加顶点变换矩阵
在顶点着色器代码中添加用于变换的矩阵uMVPMatrix
//顶点着色代码
final String vsh = "#version 300 es\n" +
"layout (location = 0) in vec3 aPosition; \n" +
"layout (location = 1) in vec4 aColor;\n" +
"uniform mat4 uMVPMatrix;\n" +
"out vec4 color2frag;\n" +
"\n" +
"void main(){\n" +
" gl_Position = uMVPMatrix*vec4(aPosition.x,aPosition.y, aPosition.z, 1.0);\n" +
" color2frag = aColor;\n" +
"gl_PointSize=10.0;"+
"}";
---->[变换矩阵相关代码]----
private int uMVPMatrix ;//顶点变换矩阵句柄
//构造方法中获取句柄
uMVPMatrix = GLES30.glGetUniformLocation(program, "uMVPMatrix");
public void draw(float[] mvpMatrix){
//英雄所见
GLES30.glUseProgram(program);
GLES30.glUniformMatrix4fv(uMVPMatrix, 1, false,mvpMatrix, 0);
3.2 GLWorld添加变换矩阵逻辑
具体的变换细节,将在
第六集
和第七集
讲述,此处不做解释 现在视角就已经校正了
public class GLWorld extends GLSurfaceView implements GLSurfaceView.Renderer {
private static final String TAG = "GLWorld";
private GLLine line;
//Model View Projection Matrix--模型视图投影矩阵
private final float[] mMVPMatrix = new float[16];
//投影矩阵 mProjectionMatrix
private final float[] mProjectionMatrix = new float[16];
//视图矩阵 mViewMatrix
private final float[] mViewMatrix = new float[16];
public GLWorld(Context context) {
this(context, null);
}
public GLWorld(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(3);//设置OpenGL ES 3.0 context
setRenderer(this);//设置渲染器
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
line = new GLLine();
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES30.glViewport(0, 0, width, height);//设置GL视口
float ratio = (float) width / height;
//透视投影矩阵--截锥
Matrix.frustumM(mProjectionMatrix, 0,
-ratio, ratio, -1, 1,
3, 7);
// 设置相机位置(视图矩阵)
Matrix.setLookAtM(mViewMatrix, 0,
0, 0, 4,
0f, 0f, 0f,
0f, 1.0f, 0.0f);
}
@Override
public void onDrawFrame(GL10 gl) {
Matrix.multiplyMM(
mMVPMatrix, 0,
mProjectionMatrix, 0,
mViewMatrix, 0);
line.draw(mMVPMatrix);
}
}
4.精简和优化
4.1 着色器shader的独立文件
着色器shader是OpenGL灵魂般的存在,所以直接写在代码里肯定不太好
一般放在assets文件夹里,另外值得一提的是AS的着色器代码高亮显示插件
个人习惯片段用.fsh
的后缀名,顶点用.vsh
的后缀名
4.2 读取资源文件并加载程序方法
这些通用的不变的操作可以提取出来进行复用
public static int initProgramByAssets(Context ctx, String vertName, String fragName){
int program=-1;
////顶点着色
int vertexShader = loadShaderAssets(ctx, GLES30.GL_VERTEX_SHADER, vertName);
//片元着色
int fragmentShader = loadShaderAssets(ctx, GLES30.GL_FRAGMENT_SHADER, fragName);
program = GLES30.glCreateProgram();//创建空的OpenGL ES 程序
GLES30.glAttachShader(program, vertexShader);//加入顶点着色器
GLES30.glAttachShader(program, fragmentShader);//加入片元着色器
GLES30.glLinkProgram(program);//创建可执行的OpenGL ES项目
return program;
}
//从sh脚本中加载shader内容的方法
private static int loadShaderAssets(Context ctx, int type, String name) {
byte[] buf = new byte[1024];
StringBuilder sb = new StringBuilder();
int len;
try (InputStream is = ctx.getAssets().open(name)) {
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
} catch (Exception e) {
e.printStackTrace();
}
return loadShader(type, sb.toString());
}
使用时直接加载即可,这样GLLine的内容就比较精简了。
program = GLLoader.initProgramByAssets(context, "base.vsh", "base.fsh");
这样也能正常表现,以后对着色器代码的修改就是家常便饭。
本篇为你介绍了OpenGLES的基础使用,旨在为你打开一扇OpenGLES的大门,其中很多细节一言蔽之,后面会一一道来。所以这一篇可以不求甚解,跑出来就算你成功了。下一篇将会为你介绍面的绘制和贴图,这是OpenGLES和图片关联起来的重要部分。