概述
这是一个新的系列,学习OpengGl Es,其实是《OpenGl Es 应用开发实践指南 Android卷》的学习笔记,感兴趣的可以直接看这本书,当然这个会记录自己的理解,以下只作为笔记,以防以后忘记
之后会对本书的前九章依次分析记录
Android OpenGl Es 学习(一):创建一个OpenGl es程序
Android OpenGl Es 学习(二):定义顶点和着色器
Android OpenGl Es 学习(八):构建简单物体
新年的第一篇文章
本章要完成的效果,直接上图
画圆
前面我们已经知道opengl三角形扇的画法,圆可以看成以圆心为中心点三角形扇,如图:
圆的内部是一个正多边形,当我们的正多边形的边数(或者三角形的个数)足够多的话,我们肉眼看起来就是一个圆
假设我们的圆心为(x,y),圆的半径为r,我们需要计算的就是周围点的坐标
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EDWFrS9H-1613961711034)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08fc6138de534201af900949f3073fd6~tplv-k3u1fbpfcp-watermark.image)]
我们很容易计算出来A点的坐标 :
A点的横坐标为: x + r * cos θ
A点的纵坐标为: y + r * sin θ
我们如果将圆分成n分的话,就可以得到每一个角度的值,一个for循环就可以得到所有的坐标点
看下代码
/**
* 生成圆的顶点数据
*
* @return 返回圆的顶点坐标
*/
public float[] getVertexData() {
//切分为count个三角形,需要一个重复的顶点和一个圆心顶点,所以需要加2
int nodeCount = count + 2;
//储存顶点数据的容器
float[] vertexData = new float[nodeCount * POSITION_COMPONENT_COUNT];
int offset = 0;
vertexData[offset++] = x;
vertexData[offset++] = y;
for (int i = 0; i < count + 1; i++) {
float angleInRadians = ((float) i / (float) count)
* ((float) Math.PI * 2f);
vertexData[offset++] = x + r * (float) Math.cos(angleInRadians);
vertexData[offset++] = y + r * (float) Math.sin(angleInRadians);
}
return vertexData;
}
假如我们分成count个三角形,那么需要的顶点数就是count + 2
,切分为count个三角形,需要一个重复的顶点和一个圆心顶点,所以需要加2
然后就是计算角度,计算每个点的坐标
下面看下完整代码
public class Circle {
// 每个顶点包含的数据个数 ( x 和 y )
private static final int POSITION_COMPONENT_COUNT = 2;
//每个顶点占用4个字节
private static final int BYTES_PER_FLOAT = 4;
private FloatBuffer floatBuffer;
private int program;
private int a_position;
private int u_matrix;
private float[] mProjectionMatrix = new float[16];
//圆心x坐标
private float x = 0;
//圆心y坐标
private float y = 0;
//圆半径
private float r = 0.6f;
//三角形的个数
private int count = 50;
private int u_color;
public void init(Context context) {
//1 生成顶点
float[] vertexData = getVertexData();
//2 加载顶点到本地内存
initVertexData(vertexData);
//3 加载着色器的源码并且加载程序
loadShaderAndProgram(context);
//4 加载着色器中的属性
loadShaderAttributes();
//5 把着色器属性和顶点数据绑定起来,开启使用顶点
bindAttributes();
}
/**
* 生成圆的顶点数据
*
* @return 返回圆的顶点坐标
*/
public float[] getVertexData() {
//切分为count个三角形,需要一个重复的顶点和一个圆心顶点,所以需要加2
int nodeCount = count + 2;
//储存顶点数据的容器
float[] vertexData = new float[nodeCount * POSITION_COMPONENT_COUNT];
int offset = 0;
vertexData[offset++] = x;
vertexData[offset++] = y;
for (int i = 0; i < count + 1; i++) {
float angleInRadians = ((float) i / (float) count)
* ((float) Math.PI * 2f);
vertexData[offset++] = x + r * (float) Math.cos(angleInRadians);
vertexData[offset++] = y + r * (float) Math.sin(angleInRadians);
}
return vertexData;
}
/**
* 把顶点数据加载到本地内存中
*
* @param vertexData 顶点数据
*/
public void initVertexData(float[] vertexData) {
floatBuffer = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
floatBuffer.position(0);
}
/**
* 加载着色器的源码并且加载程序
*/
public void loadShaderAndProgram(Context context) {
//读取着色器源码
String fragment_shader_source = ReadResouceText.readResoucetText(context, R.raw.simple_fragment_shader);
String vertex_shader_source = ReadResouceText.readResoucetText(context, R.raw.simple_vertex_shader);
//编译着色器源码
int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
//链接程序
program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);
//验证opengl对象
ShaderHelper.volidateProgram(program);
//使用程序
GLES20.glUseProgram(program);
}
/**
* 加载着色器中的属性
*/
public void loadShaderAttributes() {
//获取shader属性
u_color = GLES20.glGetUniformLocation(program, "u_Color");
a_position = GLES20.glGetAttribLocation(program, "a_Position");
u_matrix = GLES20.glGetUniformLocation(program, "u_Matrix");
}
/**
* 把着色器属性和顶点数据绑定起来,开启使用顶点
*/
public void bindAttributes() {
//绑定a_position和verticeData顶点位置
/**
* 第一个参数,这个就是shader属性
* 第二个参数,每个顶点有多少分量,我们这个只有来个分量
* 第三个参数,数据类型
* 第四个参数,只有整形才有意义,忽略
* 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
* 第六个参数,opengl从哪里读取数据
*/
floatBuffer.position(0);
GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, 0, floatBuffer);
//开启顶点
GLES20.glEnableVertexAttribArray(a_position);
}
/**
* 根据屏幕宽高创建正交矩阵,修复宽高比问题
*
* @param width 屏幕宽
* @param height 屏幕高
*/
public void projectionMatrix(int width, int height) {
float a = width > height ? (float) width / (float) height : (float) height / (float) width;
if (width > height) {
Matrix.orthoM(mProjectionMatrix, 0, -a, a, -1f, 1f, -1f, 1f);
} else {
Matrix.orthoM(mProjectionMatrix, 0, -1f, 1f, -a, a, -1f, 1f);
}
}
/**
* 开始画圆
*/
public void draw() {
//设置圆的颜色 红色
GLES20.glUniform4f(u_color, 1.0f, 1.0f, 1.0f, 1f);
//设置矩阵数据
GLES20.glUniformMatrix4fv(u_matrix, 1, false, mProjectionMatrix, 0);
/**
* 第一个参数:绘制绘制三角形
* 第二个参数:从顶点数组0索引开始读
* 第三个参数:读入几个顶点
*
* 最终绘制成圆
*/
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, count + 2);
}
}
public class AirHockKeyRenderCircle implements GLSurfaceView.Renderer {
private final Context mContext;
private Circle circle;
private Cylinder cylinder;
//画圆
public AirHockKeyRenderCircle(Context context) {
this.mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//当surface被创建时,GlsurfaceView会调用这个方法,这个发生在应用程序
// 第一次运行的时候或者从其他Activity回来的时候也会调用
//清空屏幕
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
circle = new Circle();
circle.init(mContext);
// cylinder = Cylinder.getInstance();
// cylinder.init(mContext);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在Surface创建以后,每次surface尺寸大小发生变化,这个方法会被调用到,比如横竖屏切换
//设置屏幕的大小
GLES20.glViewport(0, 0, width, height);
// cylinder.projectionMatrix(width, height);
circle.projectionMatrix(width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
//当绘制每一帧数据的时候,会调用这个放方法,这个方法一定要绘制一些东西,即使只是清空屏幕
//因为这个方法返回后,渲染区的数据会被交换并显示在屏幕上,如果什么都没有话,会看到闪烁效果
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// cylinder.draw();
circle.draw();
}
}
/simple_fragment_shader.glsl
precision mediump float;
uniform vec4 u_Color;
void main() {
gl_FragColor = u_Color;
}
/simple_vertex_shader.glsl
attribute vec4 a_Position;
uniform mat4 u_Matrix;
void main() {
gl_Position = u_Matrix * a_Position;
}
看下效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6lYpAQ52-1613961711036)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8466f95e594b4d019493baa7e136d233~tplv-k3u1fbpfcp-watermark.image)]
总结opengl书写流程
- 首先要生成顶点数据
- 加载顶点到本地内存
- 加载着色器的源码并且加载程序
- 加载着色器中的属性
- 给着色器属性赋值
- 开始绘画
画圆柱
要构建一个圆柱首先我们先把圆柱拆分出来,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TagEV2LM-1613961711037)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d082818845c4708ad3e120839912431~tplv-k3u1fbpfcp-watermark.image)]
我们可以看到,一个圆柱展开其实就是俩个圆,和一个矩形,圆的画法上面已经说过了
要构建圆柱的侧面,我们可以用一个相关的概念,他被称为三角形带,三角形带可以定义多个三角形而不用一遍又一遍的重复写三角形中那些重复的点,三角形带中的三角形彼此相邻放置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OaP8zZHq-1613961711039)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0c8ae478c5694a89b13aac2873f8c493~tplv-k3u1fbpfcp-watermark.image)]
三角形带前三个顶点定义了第一个三角形,之后每一个顶点定义另一个三角形,为了使用三角形带定义这个圆柱体的侧面,我们只需要把这个带卷成一个管子,并确保最后俩个顶点和最前面俩个顶点相同
添加点 圆 圆柱对象
public class Geometry {
//点
public static class Point {
public final float x, y, z;
public Point(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
}
public Point translateY(float distance) {
return new Point(x, y + distance, z);
}
}
//圆
public static class Circle {
public final Point center;
public final float radius;
public Circle(Point center, float radius) {
this.center = center;
this.radius = radius;
}
public Circle scale(float scale) {
return new Circle(center, radius * scale);
}
}
//圆柱
public static class Cylinder {
public final Point center;
public final float radius;
public final float height;
public Cylinder(Point center, float radius, float height) {
this.center = center;
this.radius = radius;
this.height = height;
}
}
}
开始画圆柱
private static int sizeOfCricleInVerTices(int number) {
//切分为number个三角形,需要一个重复的顶点和一个圆心顶点,所以需要加2
return 1 + number + 1;
}
private static int sizeOfCylinderInVerTices(int number) {
//围绕顶部圆的每一个顶点,都需要俩个顶点,并且前俩个顶点需要重复俩次才能闭合
return (number + 1) * 2;
}
圆柱的侧面,是一个卷起来的长方形,围绕顶部圆的每一个顶点,都需要俩个顶点(上圆的顶点和下圆的顶点,他们x和z相同,只有y不同),并且前俩个顶点需要重复俩次才能闭合
//创建圆柱
public void createPuck(Geometry.Cylinder puck, int number) {
//计算需要的画圆柱一共需要的顶点数
int size = sizeOfCricleInVerTices(number) * 2 + sizeOfCylinderInVerTices(number);
vertextData = new float[size * POSITION_COMPONENT_COUNT];
//创建顶部圆
Geometry.Circle puckTop = new Geometry.Circle(puck.center.translateY(puck.height/2), puck.radius);
//创建底部圆
Geometry.Circle puckTop1 = new Geometry.Circle(puck.center.translateY(-puck.height/2), puck.radius);
//绘制侧面
appendCylinder(puck, number);
//绘制顶部圆
appendCircle(puckTop, number, true);
//绘制底部圆
appendCircle(puckTop1, number, false);
}
首先计算需要的顶点数,需要俩个圆所以*2
然后创建顶部和底部圆,他们的圆心就是圆柱的圆心Y轴加上或者减圆柱的高度的一半
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TOuwaL3L-1613961711039)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6173ab9a6a714bef8149c0762f938f21~tplv-k3u1fbpfcp-watermark.image)]
然后绘制侧面,最后绘制上下俩个圆
用三角形扇构建圆
private void appendCircle(Geometry.Circle circle, int number, final boolean color) {
final int startVertex = offerset / FLOATS_PER_VERTEX;
final int numberVertices = sizeOfCricleInVerTices(number);
vertextData[offerset++] = circle.center.x;
vertextData[offerset++] = circle.center.y;
vertextData[offerset++] = circle.center.z;
for (int i = 0; i <= number; i++) {
//计算每个圆心角的角度
float angle = ((float) i / (float) number) * ((float) Math.PI * 2f);
vertextData[offerset++] = circle.center.x + circle.radius * (float) Math.cos(angle);
vertextData[offerset++] = circle.center.y;
vertextData[offerset++] = circle.center.z + circle.radius * (float) Math.sin(angle);
// Log.d("mmm1最后", offerset + "/");
}
Log.d("mmm1", startVertex + "/" + numberVertices + color);
drawList.add(new DrawCommand() {
@Override
public void draw() {
if (color) {
GLES20.glUniform4f(u_color, 0.0f, 1.0f, 0.0f, 1f);
} else {
GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1f);
}
//用三角形扇绘画
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, startVertex, numberVertices);
}
});
}
这个和上面讲的有一点不同,就是上面那个是二维平面,这个是三维平面的圆,这个圆是在x,z平面的,所以,用x,z来计算
最后用三角形扇绘画
用三角形带绘制圆柱侧面
public void appendCylinder(Geometry.Cylinder cylinder, int number) {
final int startVertex = offerset / FLOATS_PER_VERTEX;
Log.d("mmm1怎么回事", offerset + "/");
final int numberVertices = sizeOfCylinderInVerTices(number);
final float yStart = cylinder.center.y - cylinder.height / 2;
final float yEed = cylinder.center.y + cylinder.height / 2;
for (int i = 0; i <= number; i++) {
float angle = ((float) i / (float) number) * ((float) Math.PI * 2f);
float xPosition = cylinder.center.x + cylinder.radius * (float) Math.cos(angle);
float zPosition = cylinder.center.z + cylinder.radius * (float) Math.sin(angle);
vertextData[offerset++] = xPosition;
vertextData[offerset++] = yStart;
vertextData[offerset++] = zPosition;
vertextData[offerset++] = xPosition;
vertextData[offerset++] = yEed;
vertextData[offerset++] = zPosition;
}
Log.d("mmm2", startVertex + "/" + numberVertices);
drawList.add(new DrawCommand() {
@Override
public void draw() {
GLES20.glUniform4f(u_color, 1.0f, 1.0f, 1.0f, 1f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, startVertex, numberVertices);
}
});
}
其实每计算一次,就对应俩个顶点,一个在顶部,一个在底部,他们的x,z相同,只有y不同,所以我们先计算出顶部和底部y的值,yEnd和yStart
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbAZVJY3-1613961711040)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dd33d7d4f28540848dac23a38856aeca~tplv-k3u1fbpfcp-watermark.image)]
接下来生成使用了和上面生成圆同样的算法,只是这个我们每生成一个点,就为俩个顶点赋值,一个是圆的顶部,一个是圆的底部,最前面俩个点重复俩次,使圆闭合
添加相机(视图矩阵)
我们还需要通过视图矩阵添加相机的概念
我们从最早开始没有任何矩阵,然后添加正交矩阵
解决屏幕宽高比问题,接着切换到透视矩阵
获得一个三维投影,之后添加一个模型矩阵
开始来回移动物体,视图矩阵
只是模型矩阵
的一个扩展,他们出于同样的目的,只不过视图矩阵
平等的应用于场景中的每一个物体
简单的矩阵层次
- 模型矩阵
他可以把物体放在世界坐标系,比如我们有圆柱模型和立方体模型,他们的初始中心点都在(0,0,0)处,没有模型矩阵他们就会卡在哪里,如果我们要移动他,我们就不得不更新每个矩阵的顶点,如果不想这么做,可以使用模型矩阵,把这些顶点和矩阵相乘,来变化他们的位置
- 视图矩阵
视图矩阵是出于和模型矩阵一样的原因被使用的,他影响场景中每一个物体,因为他影响所有的物体,所以在功能上他相当于一个相机,来回移动相机,你就会从不同的视角看见那些东西
- 投影矩阵
这个矩阵帮助创建三维幻想
一个顶点如何从原来位置变换到屏幕
- vertexmodel
这是模型坐标系的一个顶点,就是我们在代码中定义的顶点数据
- vertexworld
这个是用模型矩阵
定位过的一个顶点,世界坐标系
- vertexeye
这是与我们眼睛或者相机对应的一个顶点,我们使用视图矩阵
可以得到
- vertexclip
这是被投影矩阵
处理过的顶点,下一步就是做透视除法
- vertexndc
这是归一化坐标的一个顶点,一旦一个顶点落入这个坐标,opengl就会把他映射到窗口,你就能在屏幕看到他了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eioVIVh4-1613961711041)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8796bcc5f52f446c95c5e2d9ac106884~tplv-k3u1fbpfcp-watermark.image)]
其实我们可以看到我们可以操控其实就是这3个矩阵,其他都是事情都是系统自动完成的
初始化视图矩阵
/**
* 根据屏幕宽高创建正交矩阵,修复宽高比问题
*
* @param width 屏幕宽
* @param height 屏幕高
*/
public void projectionMatrix(int width, int height) {
//45度视野角创建一个透视投影,这个视椎体从z轴-1开始,-10结束
MatrixHelper.perspetiveM(mProjectionMatrix, 45, (float) width / (float) height, 1f, 10f);
//创建视图矩阵
Matrix.setLookAtM(mViewMatrix, 0, 0f, 2f, 2f, 0f, 0f,
0f, 0f, 1f, 0f);
}
看下详细的方法api
public static void setLookAtM(float[] rm, int rmOffset,
float eyeX, float eyeY, float eyeZ,
float centerX, float centerY, float centerZ, float upX, float upY,
float upZ) {
参数 | 介绍 |
---|---|
float[] rm | 这个是目标数组,这个数组至少容纳16个元素,以便他储存试图矩阵 |
int rmOffset | 会从这个偏移值储存到rm |
float eyeX, float eyeY, float eyeZ | 这个是眼睛所在的位置,场景中所有的东西都是从这个点来观察他 |
float centerX, float centerY, float centerZ | 这个是眼睛正在看的地方,这个位置一版在整个场景的中心 |
float upX, float upY,float upZ | 刚才讨论的是你的眼睛,这个就是指的你的头指向的位置,upY=1说明给你你的头笔直向上 |
上方参数设置eye的位置为(0f, 2f, 2f)这意味者眼睛的位置在x-z平面上方2f,向后移动2个单位,也就是说场景中所有的物体,都出现在你下面的2个单位,你前面的2个单位,把center设置为(0,0,0)表示你向下看向原点,并把up设置为(0,1,0),意味着你的头是笔直向上的,这个场景不会旋转到任何一边
绘画
public void draw() {
//矩阵相乘
Matrix.multiplyMM(mViewProjectionMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
positionTableInScreen();
GLES20.glUniformMatrix4fv(u_matrix, 1, false, mViewModelProjectionMatrix, 0);
// Log.d("mmm", drawList.size() + "/");
for (DrawCommand command : drawList) {
command.draw();
}
}
private void positionTableInScreen() {
//矩阵相乘
Matrix.multiplyMM(mViewModelProjectionMatrix, 0, mViewProjectionMatrix,
0, mModelMatrix, 0);
}
这个很简单,就是先把你需要的矩阵全部相乘,得到最终的矩阵,然后在开始画圆柱
增加触摸反馈
其实和平时android中的触摸反馈都差不多
glSurfaceView = findViewById(R.id.glsurface);
glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();//当前的触控位置X坐标
float y = event.getY();//当前的触控位置X坐标
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Cylinder.getInstance().rotate(10, 1, 0, 0);
// Cylinder.getInstance().scale(0.4f, 1.5f, 0.6f);//xyz三个方向按各自的缩放因子进行缩放
break;
case MotionEvent.ACTION_MOVE://检测到移动事件时
float dx = x - mPreviousX;
float dy = y - mPreviousY;
if (dx > 0) {
Cylinder.getInstance().rotate(-dx, 1, 0, 0);
// Cylinder.getInstance().translate(0.1f, 0, 0);
} else {
Cylinder.getInstance().rotate(dx, 1, 0, 0);
// Cylinder.getInstance().translate(-0.1f, 0, 0);
}
if (dy > 0) {
Cylinder.getInstance().rotate(-dy, 0, 0, 1);
} else {
Cylinder.getInstance().rotate(dy, 0, 0, 1);
}
break;
}
mPreviousX = x;
mPreviousY = y;
return true;
}
});
就是添加setOnTouchListener
然后监听ACTION_MOVE
事件,然后做出自己的操作就可以,比如这里,及时当MOVE
的时候把圆柱旋转,运行之后,就是本文章第一张图的效果了
完整代码
public class Cylinder {
// 每个顶点包含的数据个数 ( x 和 y )
private static final int POSITION_COMPONENT_COUNT = 3;
//每个顶点占用4个字节
private static final int BYTES_PER_FLOAT = 4;
public static final int FLOATS_PER_VERTEX = 3;
private FloatBuffer floatBuffer;
private int program;
private int a_position;
private int u_matrix;
private float[] mProjectionMatrix = new float[16];
private float[] mModelMatrix = new float[16];
//视图矩阵
private float[] mViewMatrix = new float[16];
private float[] mViewProjectionMatrix = new float[16];
private float[] mViewModelProjectionMatrix = new float[16];
private static Cylinder cylinder = new Cylinder();
//圆心x坐标
private float x = 0;
//圆心y坐标
private float y = 0;
//圆半径
private float r = 0.6f;
//三角形的个数
private int count = 50;
private int u_color;
private int offerset;
private float[] vertextData;
private ArrayList<DrawCommand> drawList = new ArrayList<>();
private Cylinder() {
}
public static Cylinder getInstance() {
return cylinder;
}
public void init(Context context) {
//设置为单位矩阵
Matrix.setIdentityM(mModelMatrix, 0);
//1 生成顶点
//2 加载顶点到本地内存
Geometry.Point point = new Geometry.Point(0f, 0f, 0f);
Geometry.Cylinder cylinder = new Geometry.Cylinder(point, 0.4f, 0.5f);
createPuck(cylinder, 50);
initVertexData(vertextData);
//3 加载着色器的源码并且加载程序
loadShaderAndProgram(context);
//4 加载着色器中的属性
loadShaderAttributes();
//5 把着色器属性和顶点数据绑定起来,开启使用顶点
bindAttributes();
}
private static int sizeOfCricleInVerTices(int number) {
//切分为number个三角形,需要一个重复的顶点和一个圆心顶点,所以需要加2
return 1 + number + 1;
}
private static int sizeOfCylinderInVerTices(int number) {
//围绕顶部圆的每一个顶点,都需要俩个顶点,并且前俩个顶点需要重复俩次才能闭合
return (number + 1) * 2;
}
//创建圆柱
public void createPuck(Geometry.Cylinder puck, int number) {
//计算需要的画圆柱一共需要的顶点数
int size = sizeOfCricleInVerTices(number) * 2 + sizeOfCylinderInVerTices(number);
vertextData = new float[size * POSITION_COMPONENT_COUNT];
//创建顶部圆
Geometry.Circle puckTop = new Geometry.Circle(puck.center.translateY(puck.height/2), puck.radius);
//创建底部圆
Geometry.Circle puckTop1 = new Geometry.Circle(puck.center.translateY(-puck.height/2), puck.radius);
//绘制侧面
appendCylinder(puck, number);
//绘制顶部圆
appendCircle(puckTop, number, true);
//绘制底部圆
appendCircle(puckTop1, number, false);
}
private void appendCircle(Geometry.Circle circle, int number, final boolean color) {
final int startVertex = offerset / FLOATS_PER_VERTEX;
final int numberVertices = sizeOfCricleInVerTices(number);
vertextData[offerset++] = circle.center.x;
vertextData[offerset++] = circle.center.y;
vertextData[offerset++] = circle.center.z;
for (int i = 0; i <= number; i++) {
//计算每个圆心角的角度
float angle = ((float) i / (float) number) * ((float) Math.PI * 2f);
vertextData[offerset++] = circle.center.x + circle.radius * (float) Math.cos(angle);
vertextData[offerset++] = circle.center.y;
vertextData[offerset++] = circle.center.z + circle.radius * (float) Math.sin(angle);
// Log.d("mmm1最后", offerset + "/");
}
Log.d("mmm1", startVertex + "/" + numberVertices + color);
drawList.add(new DrawCommand() {
@Override
public void draw() {
if (color) {
GLES20.glUniform4f(u_color, 0.0f, 1.0f, 0.0f, 1f);
} else {
GLES20.glUniform4f(u_color, 1.0f, 0.0f, 0.0f, 1f);
}
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, startVertex, numberVertices);
}
});
}
public void appendCylinder(Geometry.Cylinder cylinder, int number) {
final int startVertex = offerset / FLOATS_PER_VERTEX;
Log.d("mmm1怎么回事", offerset + "/");
final int numberVertices = sizeOfCylinderInVerTices(number);
final float yStart = cylinder.center.y - cylinder.height / 2;
final float yEed = cylinder.center.y + cylinder.height / 2;
for (int i = 0; i <= number; i++) {
float angle = ((float) i / (float) number) * ((float) Math.PI * 2f);
float xPosition = cylinder.center.x + cylinder.radius * (float) Math.cos(angle);
float zPosition = cylinder.center.z + cylinder.radius * (float) Math.sin(angle);
vertextData[offerset++] = xPosition;
vertextData[offerset++] = yStart;
vertextData[offerset++] = zPosition;
vertextData[offerset++] = xPosition;
vertextData[offerset++] = yEed;
vertextData[offerset++] = zPosition;
}
Log.d("mmm2", startVertex + "/" + numberVertices);
drawList.add(new DrawCommand() {
@Override
public void draw() {
GLES20.glUniform4f(u_color, 1.0f, 1.0f, 1.0f, 1f);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, startVertex, numberVertices);
}
});
}
/**
* 把顶点数据加载到本地内存中
*
* @param vertexData 顶点数据
*/
public void initVertexData(float[] vertexData) {
floatBuffer = ByteBuffer
.allocateDirect(vertexData.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(vertexData);
floatBuffer.position(0);
}
/**
* 加载着色器的源码并且加载程序
*/
public void loadShaderAndProgram(Context context) {
//读取着色器源码
String fragment_shader_source = ReadResouceText.readResoucetText(context, R.raw.simple_fragment_shader);
String vertex_shader_source = ReadResouceText.readResoucetText(context, R.raw.simple_vertex_shader);
//编译着色器源码
int mVertexshader = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, vertex_shader_source);
int mFragmentshader = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, fragment_shader_source);
//链接程序
program = ShaderHelper.linkProgram(mVertexshader, mFragmentshader);
//验证opengl对象
ShaderHelper.volidateProgram(program);
//使用程序
GLES20.glUseProgram(program);
}
/**
* 加载着色器中的属性
*/
public void loadShaderAttributes() {
//获取shader属性
u_color = GLES20.glGetUniformLocation(program, "u_Color");
a_position = GLES20.glGetAttribLocation(program, "a_Position");
u_matrix = GLES20.glGetUniformLocation(program, "u_Matrix");
}
/**
* 把着色器属性和顶点数据绑定起来,开启使用顶点
*/
public void bindAttributes() {
//绑定a_position和verticeData顶点位置
/**
* 第一个参数,这个就是shader属性
* 第二个参数,每个顶点有多少分量,我们这个只有来个分量
* 第三个参数,数据类型
* 第四个参数,只有整形才有意义,忽略
* 第5个参数,一个数组有多个属性才有意义,我们只有一个属性,传0
* 第六个参数,opengl从哪里读取数据
*/
floatBuffer.position(0);
GLES20.glVertexAttribPointer(a_position, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
false, 0, floatBuffer);
//开启顶点
GLES20.glEnableVertexAttribArray(a_position);
}
/**
* 根据屏幕宽高创建正交矩阵,修复宽高比问题
*
* @param width 屏幕宽
* @param height 屏幕高
*/
public void projectionMatrix(int width, int height) {
//45度视野角创建一个透视投影,这个视椎体从z轴-1开始,-10结束
MatrixHelper.perspetiveM(mProjectionMatrix, 45, (float) width / (float) height, 1f, 10f);
//创建视图矩阵
Matrix.setLookAtM(mViewMatrix, 0, 0f, 2f, 2f, 0f, 0f,
0f, 0f, 1f, 0f);
}
/**
* 开始画圆
*/
public void draw() {
//设置圆的颜色 红色
//设置矩阵数据
Matrix.multiplyMM(mViewProjectionMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
positionTableInScreen();
GLES20.glUniformMatrix4fv(u_matrix, 1, false, mViewModelProjectionMatrix, 0);
// Log.d("mmm", drawList.size() + "/");
for (DrawCommand command : drawList) {
command.draw();
}
}
private void positionTableInScreen() {
//矩阵相乘
Matrix.multiplyMM(mViewModelProjectionMatrix, 0, mViewProjectionMatrix,
0, mModelMatrix, 0);
}
public void translate(float x, float y, float z)//设置沿xyz轴移动
{
Log.d("mmm", "平移");
Matrix.translateM(mModelMatrix, 0, x, y, z);
}
//旋转变换
public void rotate(float angle, float x, float y, float z) {
// 设置绕xyz轴移动
Log.d("mmm", "旋转");
Matrix.rotateM(mModelMatrix, 0, angle, x, y, z);
}
//缩放变换
public void scale(float x,float y,float z) {
Matrix.scaleM(mModelMatrix, 0, x, y, z);
}
public interface DrawCommand {
void draw();
}
}
public class AirHockKeyRenderCircle implements GLSurfaceView.Renderer {
private final Context mContext;
private Circle circle;
private Cylinder cylinder;
//画圆
public AirHockKeyRenderCircle(Context context) {
this.mContext = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//当surface被创建时,GlsurfaceView会调用这个方法,这个发生在应用程序
// 第一次运行的时候或者从其他Activity回来的时候也会调用
//清空屏幕
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
// circle = new Circle();
// circle.init(mContext);
cylinder = Cylinder.getInstance();
cylinder.init(mContext);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//在Surface创建以后,每次surface尺寸大小发生变化,这个方法会被调用到,比如横竖屏切换
//设置屏幕的大小
GLES20.glViewport(0, 0, width, height);
cylinder.projectionMatrix(width, height);
// circle.projectionMatrix(width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
//当绘制每一帧数据的时候,会调用这个放方法,这个方法一定要绘制一些东西,即使只是清空屏幕
//因为这个方法返回后,渲染区的数据会被交换并显示在屏幕上,如果什么都没有话,会看到闪烁效果
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
cylinder.draw();
// circle.draw();
}
}
public class MainActivity extends AppCompatActivity {
private GLSurfaceView glSurfaceView;
private float mPreviousX;
private float mPreviousY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initData() {
if (supportsEs2()) {
AirHockKeyRenderCircle myGlRender = new AirHockKeyRenderCircle(this);
//设置opengl版本
glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setRenderer(myGlRender);
//RenderMode 有两种,RENDERMODE_WHEN_DIRTY 和 RENDERMODE_CONTINUOUSLY,前者是懒惰渲染,需要手动调用
// glSurfaceView.requestRender() 才会进行更新,而后者则是不停渲染。
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
} else {
Log.d("mmm", "不支持2.0版本");
}
}
private void initView() {
glSurfaceView = findViewById(R.id.glsurface);
glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();//当前的触控位置X坐标
float y = event.getY();//当前的触控位置X坐标
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Cylinder.getInstance().rotate(10, 1, 0, 0);
// Cylinder.getInstance().scale(0.4f, 1.5f, 0.6f);//xyz三个方向按各自的缩放因子进行缩放
break;
case MotionEvent.ACTION_MOVE://检测到移动事件时
float dx = x - mPreviousX;
float dy = y - mPreviousY;
if (dx > 0) {
Cylinder.getInstance().rotate(-dx, 1, 0, 0);
// Cylinder.getInstance().translate(0.1f, 0, 0);
} else {
Cylinder.getInstance().rotate(dx, 1, 0, 0);
// Cylinder.getInstance().translate(-0.1f, 0, 0);
}
if (dy > 0) {
Cylinder.getInstance().rotate(-dy, 0, 0, 1);
} else {
Cylinder.getInstance().rotate(dy, 0, 0, 1);
}
break;
}
mPreviousX = x;
mPreviousY = y;
return true;
}
});
}
private boolean supportsEs2() {
// Check if the system supports OpenGL ES 2.0.
ActivityManager activityManager =
(ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configurationInfo = activityManager
.getDeviceConfigurationInfo();
// Even though the latest emulator supports OpenGL ES 2.0,
// it has a bug where it doesn't set the reqGlEsVersion so
// the above check doesn't work. The below will detect if the
// app is running on an emulator, and assume that it supports
// OpenGL ES 2.0.
final boolean supportsEs2 =
configurationInfo.reqGlEsVersion >= 0x20000
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
&& (Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")));
return supportsEs2;
}
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}
@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
}
}