一、概要
上节中了解了OpenGLES最简单的使用OpenGLES2.0基础,这节在深入一个层次了解怎么去画一个三角形。对于OpenGL绘图,最主要的就是Renderer的处理。因为GLSurfaceView相当于一个View,只是显示图像,而Renderer就是GLSurfaceView要显示的图像。在Renderer中主要有三个函数onSurfaceCreated(),onSurfaceCreated()和onDrawFrame()。其中前两个函数主要用于初始化逻辑,绘图主要是在onDrawFrame()方法里面。
二、相关要点
1.绘制方式
OpenGLES图元的绘制方式有GL_POINTS 、GL_LINES、 GL_LINE_LOOP 、GL_LINE_STRIP、GL_TRIANGLES 、GL_TRIANGLE_STRIP 、GL_TRIANGLE_FAN 这几种,每个都有自己独特的作用。在使用时根据自己的需求选择不同的绘制方式进行绘制。
绘制方式 | 解释 |
GL_POINTS | 点 |
GL_LINES | 线段 |
GL_LINE_STRIP | 多段线 |
GL_LINE_LOOP | 线圈 |
GL_TRIANGLES | 三角形 |
GL_TRIANGLE_STRIP | 三角形条带 |
GL_TRIANGLE_FAN | 三角形扇 |
1. 非索引法: GLES20.glDrawArrays(GLES20.GL_POINTS, 0, vCount);
2. 索引法 :GLES20.glDrawElements(GLES20.GL_POINTS, iCount, GLES20.GL_UNSIGNED_BYTE, mIndexBuffer);
(1).GL_LINES(绘制直线)
以(v0,v1),(v2,v3),(v4,v5)的形式绘制直线。(当绘制的点只有两个时,GL_LINES与GL_LINE_STRIP绘线方式没有差异)
GLES20.glDrawArrays(GLES20.GL_LINES, 0, count);
(2).GL_LINE_STRIP(绘制直线)
以(v0,v1),(v1,v2),(v2,v3),(v3,v4),(v4,v5)的形式绘制直线。GL_LINE_LOOP以(v0,v1),(v1,v2),(v2,v3),(v3,v4),(v4,v5),(v5,v0)的形式绘制直线,比GL_LINE_STRIP多一条(v5,v0)的直线。
GLES20.glDrawArrays(GLES20.GL_LINE_STRIP, 0, count);
(3).GL_POINTS(绘制点)
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, count);
(4).GL_TRIANGLES(绘制三角形)
每三个顶之间绘制三角形,之间不连接。
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, count);
(5).GL_TRIANGLE_STRIP(绘制三角形)
它是由最少3个点构成(正好3个点就是一个GL_TRIANGLES)每增加1个点,新增的点会和之前已有的两个点构成新的三角形,依次类推。
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, count);
(6).GL_TRIANGLE_FAN(绘制三角形)
以(v0,v1,v2),(v0,v2,v3),(v0,v3,v4)的形式绘制三角形。
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, count);
2.OpenGL矩阵变换
通过模拟人眼观察物体, 可能引起屏幕图像改变的几种行为和分别对应的几种OpenGL变换,每种变换都可以由相应的Matrix静态方法生成相应的变换矩阵。 坐标系原点在屏幕中央,z轴正方向垂直屏幕向外。
行为 | 变换 | 记录变换方式的放法 | 记录变换方式的[4*4]矩阵 |
眼睛相对物体的位置改变 | 视图变换 | Matrix.setLookAtM(mVMatrix,0(偏移量),cx(相机位置x), cy, cz,tx(观察点位置x), ty, tz,upx(顶部朝向x), upy, upz) | mVMatrix[16] |
物体平移 | 模型变换 | Matrix.translateM(mMMatrix,0(偏移量),x(平移量x), y, z) | mMMatrix[16] |
物体按坐标缩放比例缩放 | 模型变换 | Matrix.scaleM(mMMatrix,sx(缩放因子),sy, sz) | mMMatrix[16] |
物体旋转 | 模型变换 | Matrix.rotateM(mMMatrix,0(偏移量),angle(旋转角度),x(需要旋转的轴), y, z) | mMMatrix[16] |
凸透镜眼睛 | 投影变换 | Matrix.frustumM(mPMatrix,0(偏移量),left,right,bottom,top,near,far//near>0) | mPMatrix[16] |
平面透镜眼睛 | 投影变换 | Matrix.orthoM(mPMatrix,0(偏移量),left,right,bottom,top,near,far//near>0) | mPMatrix[16] |
1. 变换技巧: 利用栈存储变换矩阵的算法可实现复杂场景的图形变换和提高渲染效率。
2. 变换顺序: 矩阵相乘是讲究顺序的, 比如先平移再缩放 和 先缩放再平移的效果是不同的。
三、绘制流程
- 定义物体顶点信息
- 编辑着色器代码
- 编译连接着色器程序
- 物体顶点信息传入着色器
- GLSurfaceView加载Renderer
- Activity加载GLSurfaceView
- 渲染物体输出屏幕
1.定义物体顶点信息
物体的顶点信息一般包括:(1)三维坐标信息; (2)三维法向量信息;(3)颜色信息或二维贴图坐标信息;复杂物体的坐标通常通过特定的软件绘制生成顶点信息文件,然后进行解读;
2.编辑着色器代码
着色器语言编写的代码, 通常后缀名.sh, 语法和C相似95%以上, 是OpenGLES2.0的可编程渲染通道, 增加了编程难度, 但是能实现更绚丽多彩的效果。一般编写好的着色器代码放在android工程目录下的assets文件, assets常用轻量资源的读写。
3.编译连接着色器
从assets获取着色器脚本, 并返回脚本字符串的过程
// 加载顶点着色器的脚本内容
mVertexShader = ShaderUtil.loadFromAssetsFile("graph_color_and_texture_vertex.shl",mContext.getResources());
// 加载片元着色器的脚本内容
mFragmentShader = ShaderUtil.loadFromAssetsFile("graph_color_and_texture_frag.shl",mContext.getResources());
加载片元着色器和顶点着色器
// 加载顶点着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
// 加载片元着色器
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) {
return 0;
}
// 创建程序
int program = GLES20.glCreateProgram();
// 若程序创建成功则向程序中加入顶点着色器与片元着色器
if (program != 0) {
// 向程序中加入顶点着色器
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
// 向程序中加入片元着色器
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
// 链接程序
GLES20.glLinkProgram(program);
// 存放链接成功program数量的数组
int[] linkStatus = new int[1];
// 获取program的链接情况
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
// 若链接失败则报错并删除程序
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("ES20_ERROR", "Could not link program: ");
Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
}
4. 物体顶点信息传入着色器
生成顶点信息, 用FloatBuffer类存储
ByteBuffer bbv = ByteBuffer.allocateDirect(triangleCoords.length * 4);//一个float 4个字节
bbv.order(ByteOrder.nativeOrder());
mVertexBuffer = bbv.asFloatBuffer();
mVertexBuffer.put(triangleCoords);//存入信息
mVertexBuffer.position(0);//设置起始位置为0;
获取着色器参数ID
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
传入数据
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, mVertexBuffer);
使用数据
GLES20.glEnableVertexAttribArray(maPositionHandle);
5. GLSurfaceView加载Renderer
初始化GLSurfaceView
//设置版本
setEGLContextClientVersion(2);
//创建渲染器实例
mRenderer = new MyRenderer();
// 设置渲染器
setRenderer(mRenderer);
//设置渲染模式
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
初始化Renderer
/**
* 仅调用一次,用于设置view的OpenGL ES环境
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//设置背景色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
triangle = new Triangle();
}
/**
* 当view的几何形状发生变化时调用,比如设备从竖屏变为横屏
*/
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
/**
* 每次重绘view时调用
*/
@Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
// 绘制背景
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 重绘背景色
triangle.draw(scratch);
}
7. 渲染物体输出屏幕
//编译OpenGL ES shader和链接program
ProgramFactory.initFactory(getApplicationContext());
mySurfaceView = new MySurfaceView(this);
setContentView(mySurfaceView);
四、运行效果
五、全部代码
1.在Manifests中添加
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
2.MyGLSurfaceView继承自SurfaceView
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;
/**
* Created by Administrator on 2017/3/8.
* description :
*/
public class MyGLSurfaceView extends GLSurfaceView {
MyRenderer mRenderer;
public MyGLSurfaceView(Context context) {
super(context);
init();
}
private void init(){
// 创建一个OpenGL ES 2.0 context
setEGLContextClientVersion(2);
//创建渲染器实例
mRenderer = new MyRenderer();
// 设置渲染器
setRenderer(mRenderer);
//设置渲染模式
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(mRenderer.getAngle() +((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
}
3.MyRenderer实现了GLSurfaceView.Renderer接口
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* Created by Administrator on 2017/3/8.
* description :
*/
public class MyRenderer implements GLSurfaceView.Renderer {
Triangle triangle;
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
private final float[] mRotationMatrix = new float[16];
private float mAngle;
/**
* 仅调用一次,用于设置view的OpenGL ES环境
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//设置背景色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
triangle = new Triangle();
}
/**
* 当view的几何形状发生变化时调用,比如设备从竖屏变为横屏
*/
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
/**
* 每次重绘view时调用
*/
@Override
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
// 绘制背景
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// 重绘背景色
triangle.draw(scratch);
}
public void setAngle(float angle) {
this.mAngle = angle;
}
public float getAngle() {
return mAngle;
}
}
3.Triangle主要处理画三角形逻辑
import android.opengl.GLES20;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
/**
* Created by Administrator on 2017/3/8.
* description :
*/
public class Triangle {
// GL 绘图
private FloatBuffer mVertexBuffer; // 顶点坐标数据缓冲
private int mProgram; // 自定义渲染管线程序id
private int mMVPMatrixHandle;// 总变换矩阵引用id
private int maPositionHandle; // 顶点位置属性引用id
private int maColorHandle; // 顶点颜色属性引用id
// 设置每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
// 设置三角形顶点数组
static float triangleCoords[] = { // 默认按逆时针方向顺序绘制
0.0f, 0.622008459f, 0.0f, // 顶
-0.5f, -0.311004243f, 0.0f, // 左底
0.5f, -0.311004243f, 0.0f // 右底
};
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
// 设置图形的RGB值和透明度
static float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f};
public Triangle() {
initVertexData();
initShader();
}
// 初始化顶点坐标与着色数据的方法
public void initVertexData() {
// 如果数据只有一个的时候会出错,mVertexBuffer和mColorBuffer容错10
ByteBuffer bbv = ByteBuffer.allocateDirect(triangleCoords.length * 4);//一个float 4个字节
bbv.order(ByteOrder.nativeOrder());///前期设置
mVertexBuffer = bbv.asFloatBuffer();//作为中介开辟FloatBuffer
mVertexBuffer.put(triangleCoords);
mVertexBuffer.position(0);
}
// 初始化着色器
public void initShader() {
// 加载顶点着色器的脚本内容
mProgram = ProgramFactory.shared().getProgram();
// 获取程序中顶点位置属性引用id
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 获取程序中顶点颜色属性引用id
maColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 获取程序中总变换矩阵引用id
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
}
public void draw(float[] mvpMatrix) {
// 制定使用某套shader程序
GLES20.glUseProgram(mProgram);
// 允许顶点位置数据数组
GLES20.glEnableVertexAttribArray(maPositionHandle);
// 为画笔指定顶点位置数据
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, vertexStride, mVertexBuffer);
//
GLES20.glUniform4fv(maColorHandle, 1, color, 0);
//
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
//
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
//
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
//
GLES20.glDisableVertexAttribArray(maPositionHandle);
}
}
4.在MainActivity中使用
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
/**
* Created by Administrator on 2017/3/8.
* description :
*/
public class MainActivity extends AppCompatActivity {
MyGLSurfaceView mySurfaceView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//编译OpenGL ES shader和链接program
ProgramFactory.initFactory(getApplicationContext());
mySurfaceView = new MyGLSurfaceView(this);
setContentView(mySurfaceView);
}
@Override
protected void onPause() {
super.onPause();
mySurfaceView.onPause();
}
@Override
protected void onResume() {
super.onResume();
mySurfaceView.onResume();
}
}
5.以及在assets目录下的两个着色器脚本
graph_color_and_texture_frag.shl
precision mediump float;
uniform vec4 vColor;
void main() {
gl_FragColor = vColor;
}
graph_color_and_texture_vertex.shl
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;
void main() {
gl_Position = uMVPMatrix * vPosition;
}
6.下面贴上两个OpenGL工具类
ProgramFactory
import android.content.Context;
/**
* Created by Administrator on 2017/3/8.
* description :
*/
public class ProgramFactory {
private static Context mContext = null;
String mVertexShader;
String mFragmentShader;
int mMixProgram;
public static void initFactory(Context context){
mContext = context;
}
private ProgramFactory() {
// 加载顶点着色器的脚本内容
mVertexShader = ShaderUtil.loadFromAssetsFile("graph_color_and_texture_vertex.shl",
mContext.getResources());
// 加载片元着色器的脚本内容
mFragmentShader = ShaderUtil.loadFromAssetsFile("graph_color_and_texture_frag.shl",
mContext.getResources());
// 基于顶点着色器与片元着色器创建程序
mMixProgram = ShaderUtil.createProgram(mVertexShader, mFragmentShader);
}
private static ProgramFactory mInstance;
public static ProgramFactory shared() {
if(mContext == null){
throw new IllegalStateException("ProgramFactory.initFactory() method is not transfer : mContext = null");
}
if (mInstance == null) {
mInstance = new ProgramFactory();
}
return mInstance;
}
public static void recycle() {
mInstance = null;
}
public int getProgram() {
return mMixProgram;
}
}
ShaderUtil
import android.content.res.Resources;
import android.opengl.GLES20;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
/**
* Created by Administrator on 2017/3/8.
* description :加载顶点Shader与片元Shader的工具类
*/
public class ShaderUtil {
/**
* 加载制定shader的方法
*
* @param shaderType
* shader的类型 GLES20.GL_VERTEX_SHADER GLES20.GL_FRAGMENT_SHADER
* @param source
* shader的脚本字符串
* @return
*/
public static int loadShader(int shaderType, String source) {
// 创建一个新shader
int shader = GLES20.glCreateShader(shaderType);
// 若创建成功则加载shader
if (shader != 0) {
if (source != null) {
// 加载shader的源代码
GLES20.glShaderSource(shader, source);
}
// 编译shader
GLES20.glCompileShader(shader);
// 存放编译成功shader数量的数组
int[] compiled = new int[1];
// 获取Shader的编译情况
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
// 若编译失败则显示错误日志并删除此shader
if (compiled[0] == 0) {
// Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
// Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
// 创建shader程序的方法
public static int createProgram(String vertexSource, String fragmentSource) {
// 加载顶点着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
// 加载片元着色器
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) {
return 0;
}
// 创建程序
int program = GLES20.glCreateProgram();
// 若程序创建成功则向程序中加入顶点着色器与片元着色器
if (program != 0) {
// 向程序中加入顶点着色器
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
// 向程序中加入片元着色器
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
// 链接程序
GLES20.glLinkProgram(program);
// 存放链接成功program数量的数组
int[] linkStatus = new int[1];
// 获取program的链接情况
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
// 若链接失败则报错并删除程序
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("ES20_ERROR", "Could not link program: ");
Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
// 检查每一步操作是否有错误的方法
public static void checkGlError(String op) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
// Log.e("ES20_ERROR", op + ": glError " + error);
throw new RuntimeException(op + ": glError " + error);
}
}
// 从sh脚本中加载shader内容的方法
public static String loadFromAssetsFile(String fname, Resources r) {
String result = null;
try {
InputStream in = r.getAssets().open(fname);
int ch = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((ch = in.read()) != -1) {
baos.write(ch);
}
byte[] buff = baos.toByteArray();
baos.close();
in.close();
result = new String(buff, "UTF-8");
result = result.replaceAll("\\r\\n", "\n");
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
以上即为使用OpenGLES20画三角形的全部代码