OpenglES2.0 for Android:来画个球吧
理解球坐标系
首先看下球的坐标系 ,如图 :
(图来自百度百科 )
设球上有一点 A ,球心为O ,OA在 xOy上的投影与X轴夹角为 φ (范围为 0 到360 ,单位 :度),
OA在与Z的夹角为 θ (范围为 0 到 180 ,单位:度 ),球的半径为r,则有 ;
r * sin θ = y / sin φ r * sinθ = x / cos φ z = r * cos θ
由此可得 X,Y,Z坐标
我们前面已经知道,在OpenglES中任何形状的3D物体都是用三角形来实现了,我们的球自然不能免俗,
那我们该如何用一堆三角形来完成这个球的绘制呢?回想一下我们的圆是怎么绘制的,对于圆,我们实际绘制的是一个正N边形,
当N足够大时我们看起来就是一个圆了,类比一下我们的球也是一样 :
当我们分割的足够多的时候 就像个球了:
球的顶点坐标及绘制
按照矩形的方式来看,我们绘制的球实际上是这样 :
我们实际上是想要绘制这一个个的矩形,我们把其中一个矩形单独拿来作分析 :
问题一,我们如何确定这个矩形的位置?我们只需要知道这个矩形的左上角的顶点坐标就可以确定该矩形的位置,这个顶点坐标显然与三个值有关,
角度
φ (范围为 0 到360 ,单位 :度) ,角度θ (范围为 0 到 180 ,单位:度 ),以及球的半径 r ,r可以是一个固定值 ,因此
我们通过双重循环就可以得到所有的矩形的左上角顶点坐标了。我们先写一个计算该顶点的代码 :
public void initVertexData(){
int angleSpan = 90; 将球进行切分的角度
float r = 0.6f;//球的半径
final float UNIT_SIZE = 1.0f;
for (int vAngle = 0; vAngle < 180; vAngle = vAngle + angleSpan)
{
for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan)
{
float x0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
.toRadians(hAngle)));
float y0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
.toRadians(hAngle)));
float z0 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle)));
}
}
}
每次循环我们都得到一个四边形的左上角的顶点坐标。我们知道如何求该顶点坐标后,其他三个坐标就很容易求得了,
OK,开始我们画球计划正式的第一步 ,我们直接在上节工程的基础上(我们要用到前面的工具类等,不要从零开始哦 )来做,
在shape目录下新建一个Ball类 (Ball.java):
package com.cumt.shape;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
public class Ball {
private static final float UNIT_SIZE = 1.0f;// 单位尺寸
private float r = 0.6f; // 球的半径
final int angleSpan = 10;// 将球进行单位切分的角度
private FloatBuffer vertexBuffer;// 顶点坐标
int vCount = 0;// 顶点个数,先初始化为0
// float类型的字节数
private static final int BYTES_PER_FLOAT = 4;
// 数组中每个顶点的坐标数
private static final int COORDS_PER_VERTEX = 3;
public void initVertexData() {
ArrayList<Float> alVertix = new ArrayList<Float>();// 存放顶点坐标的ArrayList
for (int vAngle = 0; vAngle < 180; vAngle = vAngle + angleSpan)// 垂直方向angleSpan度一份
{
for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan)// 水平方向angleSpan度一份
{
// 纵向横向各到一个角度后计算对应的此点在球面上的坐标
float x0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
.toRadians(hAngle)));
float y0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
.toRadians(hAngle)));
float z0 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle)));
// Log.w("x0 y0 z0","" + x0 + " "+y0+ " " +z0);
float x1 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
.toRadians(hAngle + angleSpan)));
float y1 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
.toRadians(hAngle + angleSpan)));
float z1 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle)));
// Log.w("x1 y1 z1","" + x1 + " "+y1+ " " +z1);
float x2 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.cos(Math.toRadians(hAngle + angleSpan)));
float y2 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.sin(Math.toRadians(hAngle + angleSpan)));
float z2 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle + angleSpan)));
// Log.w("x2 y2 z2","" + x2 + " "+y2+ " " +z2);
float x3 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.cos(Math.toRadians(hAngle)));
float y3 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.sin(Math.toRadians(hAngle)));
float z3 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle + angleSpan)));
// Log.w("x3 y3 z3","" + x3 + " "+y3+ " " +z3);
// 将计算出来的XYZ坐标加入存放顶点坐标的ArrayList
alVertix.add(x1);
alVertix.add(y1);
alVertix.add(z1);
alVertix.add(x3);
alVertix.add(y3);
alVertix.add(z3);
alVertix.add(x0);
alVertix.add(y0);
alVertix.add(z0);
alVertix.add(x1);
alVertix.add(y1);
alVertix.add(z1);
alVertix.add(x2);
alVertix.add(y2);
alVertix.add(z2);
alVertix.add(x3);
alVertix.add(y3);
alVertix.add(z3);
}
}
vCount = alVertix.size() / COORDS_PER_VERTEX;// 顶点的数量
// 将alVertix中的坐标值转存到一个float数组中
float vertices[] = new float[vCount * COORDS_PER_VERTEX];
for (int i = 0; i < alVertix.size(); i++) {
vertices[i] = alVertix.get(i);
}
vertexBuffer = ByteBuffer
.allocateDirect(vertices.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(vertices);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
我们使用ArrayList来保存顶点坐标,要注意顺序的问题,因为我们的四边形也是由三角形组成的,我们实际上还是绘制两个三角形。
这六个点应该应该正好组成这两个三角形。
下面我们来新建顶点着色器和片段着色器,在res / raw 目录下新建 :
<span style="font-size:14px;">//vertex_shader_ball.glsl
uniform mat4 u_Matrix;//最终的变换矩阵
attribute vec4 a_Position;//顶点位置
void main()
{
gl_Position = u_Matrix * a_Position;
} </span>
<span style="font-size:14px;">precision mediump float;
void main()
{
gl_FragColor=vec4(0.2,1.0,0.129,0);
}</span>
接下来和前面一样,编译链接~~ 等等,此时代码如下 (Ball.java ):
package com.cumt.shape;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import android.content.Context;
import android.opengl.GLES20;
import com.cumt.opengeschange.R;
import com.cumt.utils.MatrixState;
import com.cumt.utils.ShaderHelper;
import com.cumt.utils.TextResourceReader;
public class Ball {
private Context context;
private static final float UNIT_SIZE = 1.0f;// 单位尺寸
private float r = 0.6f; // 球的半径
final int angleSpan = 10;// 将球进行单位切分的角度
private FloatBuffer vertexBuffer;// 顶点坐标
int vCount = 0;// 顶点个数,先初始化为0
// float类型的字节数
private static final int BYTES_PER_FLOAT = 4;
// 数组中每个顶点的坐标数
private static final int COORDS_PER_VERTEX = 3;
private int program;
private static final String A_POSITION = "a_Position";
private static final String U_MATRIX = "u_Matrix";
private int uMatrixLocation;
private int aPositionLocation;
public Ball(Context context){
this.context = context;
initVertexData();
getProgram();
aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION);
uMatrixLocation = GLES20.glGetUniformLocation(program, U_MATRIX);
//---------传入顶点数据数据
GLES20.glVertexAttribPointer(aPositionLocation, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLES20.glEnableVertexAttribArray(aPositionLocation);
}
public void initVertexData() {
ArrayList<Float> alVertix = new ArrayList<Float>();// 存放顶点坐标的ArrayList
for (int vAngle = 0; vAngle < 180; vAngle = vAngle + angleSpan)// 垂直方向angleSpan度一份
{
for (int hAngle = 0; hAngle <= 360; hAngle = hAngle + angleSpan)// 水平方向angleSpan度一份
{
// 纵向横向各到一个角度后计算对应的此点在球面上的坐标
float x0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
.toRadians(hAngle)));
float y0 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
.toRadians(hAngle)));
float z0 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle)));
// Log.w("x0 y0 z0","" + x0 + " "+y0+ " " +z0);
float x1 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.cos(Math
.toRadians(hAngle + angleSpan)));
float y1 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle)) * Math.sin(Math
.toRadians(hAngle + angleSpan)));
float z1 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle)));
// Log.w("x1 y1 z1","" + x1 + " "+y1+ " " +z1);
float x2 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.cos(Math.toRadians(hAngle + angleSpan)));
float y2 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.sin(Math.toRadians(hAngle + angleSpan)));
float z2 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle + angleSpan)));
// Log.w("x2 y2 z2","" + x2 + " "+y2+ " " +z2);
float x3 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.cos(Math.toRadians(hAngle)));
float y3 = (float) (r * UNIT_SIZE
* Math.sin(Math.toRadians(vAngle + angleSpan)) * Math
.sin(Math.toRadians(hAngle)));
float z3 = (float) (r * UNIT_SIZE * Math.cos(Math
.toRadians(vAngle + angleSpan)));
// Log.w("x3 y3 z3","" + x3 + " "+y3+ " " +z3);
// 将计算出来的XYZ坐标加入存放顶点坐标的ArrayList
alVertix.add(x1);
alVertix.add(y1);
alVertix.add(z1);
alVertix.add(x3);
alVertix.add(y3);
alVertix.add(z3);
alVertix.add(x0);
alVertix.add(y0);
alVertix.add(z0);
alVertix.add(x1);
alVertix.add(y1);
alVertix.add(z1);
alVertix.add(x2);
alVertix.add(y2);
alVertix.add(z2);
alVertix.add(x3);
alVertix.add(y3);
alVertix.add(z3);
}
}
vCount = alVertix.size() / COORDS_PER_VERTEX;// 顶点的数量
// 将alVertix中的坐标值转存到一个float数组中
float vertices[] = new float[vCount * COORDS_PER_VERTEX];
for (int i = 0; i < alVertix.size(); i++) {
vertices[i] = alVertix.get(i);
}
vertexBuffer = ByteBuffer
.allocateDirect(vertices.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(vertices);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
//获取program
private void getProgram(){
//获取顶点着色器文本
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.vertex_shader_ball);
//获取片段着色器文本
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.fragment_shader_ball);
//获取program的id
program = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource);
GLES20.glUseProgram(program);
}
public void draw(){
//将最终变换矩阵写入
GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, MatrixState.getFinalMatrix(),0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);
}
}
package com.cumt.render;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import com.cumt.shape.Ball;
import com.cumt.utils.MatrixState;
import android.content.Context;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView.Renderer;
import android.util.Log;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glViewport;
public class MyRender implements Renderer {
private Context context;
Ball ball;
public MyRender(Context context){
this.context = context;
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.w("MyRender","onSurfaceCreated");
//设置屏幕背景色RGBA
glClearColor(0.5f,0.5f,0.5f, 1.0f);
//打开深度检测
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
//打开背面剪裁
GLES20.glEnable(GLES20.GL_CULL_FACE);
ball = new Ball(context);
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
glViewport(0,0,width,height);
float ratio = (float) width / height;
// 调用此方法计算产生透视投影矩阵
MatrixState.setProjectFrustum(-ratio,ratio, -1, 1, 20, 100);
// 调用此方法产生摄像机9参数位置矩阵
MatrixState.setCamera(0, 0, 30, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
}
public void onDrawFrame(GL10 gl) {
//清除深度缓冲与颜色缓冲
glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
ball.draw();
}
}
package com.cumt.opengeschange;
import com.cumt.render.MyRender;
import com.cumt.utils.MatrixState;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;
import android.view.View;
public class MySurfaceView extends GLSurfaceView {
private MyRender myRender;
public MySurfaceView(Context context) {
super(context);
// TODO Auto-generated constructor stub
myRender = new MyRender(context);
this.setEGLContextClientVersion(2);
this.setRenderer(myRender);
// 设置渲染模式为主动渲染
this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
this.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://检测到点击事件时
MatrixState.rotate(20f, 0, 1, 0);//绕y轴旋转
}
return true;
}
});
}
}
运行一下看下结果吧:
棋盘纹理
!!!什么状况,分明是个圆~~而且点击屏幕之后也没什么变化~~,事实上我们已经完成了球的绘制,只是没有给它光照或纹理等很难看出它是个球
我们下面来想办法使这个球更容易看出来 ,我们先不说光照 ,考虑一下棋盘纹理着色器 ,所谓棋盘纹理如下所示 :
考虑棋盘纹理的绘制策略:
想象一下我们的球是一个立方体切割出来的,这个立方体的边长就是 2 * r ,r是球的半径,我们把这个立方体切割成
一个个的小立方体,
每个小立方体的位置由其所在的层数,行数和列数共同确定,类比到球的表面,现在球的表面
是一个个分割后的小矩形,每个小矩形
的位置由其所在的层数,行数和列数共同确定。假设我们已知分割的层数,
行数和列数都是 n ,一个顶点的坐标是(x ,y,z),
求该点所在的层数,行数和列数 ? 求解过程如下 :球的半
径
为 r ,则直径为 2 * r ,则每个小正方形的边长为 2 * r / n 。该顶点在Y轴方向坐标为
y ,则其距离底层距离为 y + r ,则
其所在
层数为 (y + r )/ (
2 * r / n ),同理我们求得该点所在行数为 :
(x + r )/ (
2 * r / n ),所在列数为:
(z + r )/ (
2 * r / n )。
顶点坐标所在层数,行数,列数三者相加必定是一个奇数或者偶数,如果这三者的和是奇数则该顶点坐在片元颜色设置为
一种颜色,若这三者的和是偶数则该顶
点坐在片元颜色设置为另一种颜色,如此就完成了我们的棋盘着色策略。
下面我们来修改一下我们的着色器程序(顶点着色器和片元着色器 ),此时两者代码如下 :
//vertex_shader_ball.glsl
uniform mat4 u_Matrix;//最终的变换矩阵
attribute vec4 a_Position;//顶点位置
varying vec4 vPosition;//用于传递给片元着色器的顶点位置
void main()
{
gl_Position = u_Matrix * a_Position;
vPosition = a_Position;
}
precision mediump float;
varying vec4 vPosition;//接收从顶点着色器过来的顶点位置
void main()
{
float uR = 0.6;//球的半径
vec4 color;
float n = 8.0;//分为n层n列n行
float span = 2.0*uR/n;//正方形长度
//计算行列层数
int i = int((vPosition.x + uR)/span);//行数
int j = int((vPosition.y + uR)/span);//层数
int k = int((vPosition.z + uR)/span);//列数
int colorType = int(mod(float(i+j+k),2.0));
if(colorType == 1) {//奇数时为绿色
color = vec4(0.2,1.0,0.129,0);
}
else {//偶数时为白色
color = vec4(1.0,1.0,1.0,0);//白色
}
//将计算出的颜色给此片元
gl_FragColor=color;
}
OK,来运行下看下结果吧 :
good job !点击屏幕我们也可以看到球绕Y轴的旋转了