OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体
在Activity中监听触控事件
在Activity里,可以通过调用setOnTouchListener()监听视图的触控事件,然后转发触控事件给渲染器
在Android里,触控事件是发生在视图的坐标空间中的,视图的左上角映射到(0,0),右下角映射到的坐标等于视图的大小,而在着色器中需要使用归一化设备坐标,因此我们需要把触控事件坐标转换回归一化设备坐标,并且需要把y轴反转,并把每个坐标按比例映射到范围[-1,1]内
/**
* 处理触控事件
*/
private void dealTouchEvent(ITouchRenderer renderer) {
glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event != null) {
// 把触控事件坐标转换回归一化设备坐标
float normalizedX = event.getX() / v.getWidth() * 2 - 1;
// 把y轴反转
float normalizedY = -(event.getY() / v.getHeight() * 2 - 1);
// 转发触控事件给渲染器,通过GLSurfaceView.queueEvent()方法给OpenGL线程分发调用
if (event.getAction() == MotionEvent.ACTION_DOWN) {
glSurfaceView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.handlePress(normalizedX, normalizedY);
}
});
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
glSurfaceView.queueEvent(new Runnable() {
@Override
public void run() {
renderer.handleDrag(normalizedX, normalizedY);
}
});
}
return true;
}
return false;
}
});
}
相交测试
得到了屏幕的被触碰的归一化设备坐标,还需要使用相交测试判断被触碰的坐标是否在木槌里
相交测试步骤
- 把二维屏幕的归一化设备坐标转换为三维空间一条射线,要做到这点,我们要把被触碰的点投射到一条射线上,这条射线从我们的视点跨越那个三维场景
- 检查这条射线是否与木槌相交,为了使判断简单些,假定木槌是一个差不多同样大小的球,然后测试那个球
二维屏幕的归一化设备坐标转换为三维空间一条射线
把一个三维场景投递到二维屏幕的时候,使用透视投影和透视除法把顶点坐标变换为归一化设备坐标,为了把二维屏幕被触碰的点转换为一个三维射线(射线的近端映射到我们在投影矩阵中定义的视椎体的近平面,直线的远端映射到视椎体的远平面),需要做的就是取消透视投影和透视除法
要实现这个转换,需要一个反转的矩阵,它会取消视图矩阵和投影矩阵的效果,而 Matrix.invertM() 方法就可以获得一个矩阵的反转矩阵,方法原型
public static boolean invertM(float[] mInv, int mInvOffset, float[] m,
int mOffset) {
这个调用会创建一个反转的矩阵,我们可以用它把那个二维被触碰的点转换为两个三维坐标。如果绘制的时候使用了视图矩阵,场景可以移来移去,它就会影响场景的哪一部分在手指下面,因此需要把视图矩阵考虑在内。通过反转视图投影矩阵实现取消透视投影和透视除法
在这之前,需要先定义点,射线和向量
点
public class Point {
private float mX;
private float mY;
private float mZ;
public Point(float x, float y, float z) {
this.mX = x;
this.mY = y;
this.mZ = z;
}
public Point translate(Vector vector) {
return new Point(
mX += vector.getX(),
mY += vector.getY(),
mZ += vector.getZ()
);
}
public Point translateY(float y) {
return new Point(mX, mY + y, mZ);
}
public float getX() {
return mX;
}
public float getY() {
return mY;
}
public float getZ() {
return mZ;
}
@Override
public String toString() {
return "Point{" +
"mX=" + mX +
", mY=" + mY +
", mZ=" + mZ +
'}';
}
}
射线
public class Ray {
private Point mPoint;
private Vector mVector;
public Ray(Point point, Vector vector) {
mPoint = point;
mVector = vector;
}
public Point getPoint() {
return mPoint;
}
public Vector getVector() {
return mVector;
}
@Override
public String toString() {
return "Ray{" +
"mPoint=" + mPoint +
", mVector=" + mVector +
'}';
}
}
向量
public class Vector {
private float mX;
private float mY;
private float mZ;
public Vector(float x, float y, float z) {
this.mX = x;
this.mY = y;
this.mZ = z;
}
public static Vector vectorBetween(Point from, Point to) {
return new Vector(
to.getX() - from.getX(),
to.getY() - from.getY(),
to.getZ() - from.getZ()
);
}
public float getX() {
return mX;
}
public float getY() {
return mY;
}
public float getZ() {
return mZ;
}
public Vector crossProduct(Vector other) {
return new Vector(
mY * other.mZ - mZ * other.mY,
-(mX * other.mZ - mZ * other.mX),
mX * other.mY - mY * other.mX
);
}
public float length() {
return (float) Math.sqrt(mX * mX + mY * mY + mZ * mZ);
}
public float dotProduct(Vector other) {
return mX * other.mX + mY * other.mY + mZ * other.mZ;
}
public Vector scale(float ratio) {
return new Vector(
mX * ratio,
mY * ratio,
mZ * ratio
);
}
@Override
public String toString() {
return "Vector{" +
"mX=" + mX +
", mY=" + mY +
", mZ=" + mZ +
'}';
}
}
二维坐标转为三维射线
在 onSurfaceChanged() 方法里调用如下方法,获取视图投影矩阵的反转矩阵
Matrix.invertM(invertedViewProjectionMatrix, 0, viewProjectionMatrix, 0);
/**
* 把被触碰的点投射到一条射线上,这条射线从我们的视点跨越那个三维场景
*
* @param normalizedX
* @param normalizedY
* @return
*/
private Ray convertNormalized2DPointToRay(float normalizedX, float normalizedY) {
// 在视椎体近平面和远平面各选一个点
float[] nearPointNdc = new float[]{normalizedX, normalizedY, 1, 1};
float[] farPointNdc = new float[]{normalizedX, normalizedY, 10, 1};
float[] nearPointWorld = new float[4];
float[] farPointWorld = new float[4];
// invertedViewProjectionMatrix[0] = 0.28241837f;
// 取消所选点的视图投影、透视投影,获得世界坐标
Matrix.multiplyMV(nearPointWorld, 0, invertedViewProjectionMatrix, 0, nearPointNdc, 0);
Matrix.multiplyMV(farPointWorld, 0, invertedViewProjectionMatrix, 0, farPointNdc, 0);
// 取消透视除法的影响
divideByW(nearPointWorld);
divideByW(farPointWorld);
// 将 float[] 转为Point
Point nearPoint = new Point(nearPointWorld[0], nearPointWorld[1], nearPointWorld[2]);
Point farPoint = new Point(farPointWorld[0], farPointWorld[1], farPointWorld[2]);
return new Ray(nearPoint, Vector.vectorBetween(nearPoint, farPoint));
}
检查这条射线是否与木槌相交
为了使判断简单些,假定木槌是一个差不多同样大小的球,然后测试那个球是否与射线相交,因此需要定义球的结构
球的结构
public class Sphere {
private Point mCenter;
private float mRadius;
public Sphere(Point center, float radius) {
mCenter = center;
mRadius = radius;
}
public Point getCenter() {
return mCenter;
}
public float getRadius() {
return mRadius;
}
}
相交测试
相交测试需要一些几何判断,如下
public class Geometry {
private static final String TAG = "Geometry";
/**
* 判断球体是否与射线相交
*
* @param sphere
* @param ray
* @return
*/
public static boolean intersects(Sphere sphere, Ray ray) {
return distance(sphere.getCenter(), ray) < sphere.getRadius();
}
/**
* 计算点到射线的距离
*
* @param center
* @param ray
* @return
*/
private static float distance(Point center, Ray ray) {
Vector p1ToCenter = Vector.vectorBetween(ray.getPoint(), center);
Vector p2ToCenter = Vector.vectorBetween(ray.getPoint().translate(ray.getVector()), center);
float areaOfTriangleTimesTwo = p1ToCenter.crossProduct(p2ToCenter).length();
float lengthOfBase = ray.getVector().length();
float distance = areaOfTriangleTimesTwo / lengthOfBase;
Log.d(TAG, "distance: " + distance);
return distance;
}
}
在按下事件里判断触控点是否与物体相交
/**
* 处理按下事件
*
* @param normalizedX
* @param normalizedY
*/
@Override
public void handlePress(float normalizedX, float normalizedY) {
Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
Log.d(TAG, "handlePress: ray=" + ray);
Sphere sphere = new Sphere(new Point(malletPosition.getX(), mMallet.getHeight() / 2f, malletPosition.getZ()), mMallet.getHeight() / 2f);
Log.d(TAG, "handlePress: sphere.getRadius()=" + sphere.getRadius());
mIsPressed = Geometry.intersects(sphere, ray);
Log.d(TAG, "handlePress: mIsPressed=" + mIsPressed);
}
通过拖拽移动物体
只有当我们开始就用手指按住那个木槌时,我们才想要拖动它,因此,首先检查 malletPressed 是否为 true,如果是,那我们就做射线转换,一旦我们有了表示被触碰点的射线,我们就要找出这条射线与桌子的平面在哪里相交了,然后,把木槌移动到那个点
平面
包含一个法向向量(normal vector)和平面上的一个点,法向向量是一个垂直于那个平面的一个向量
public class Plane {
private Point mPoint;
private Vector mVector;
public Plane(Point point, Vector vector) {
mPoint = point;
mVector = vector;
}
public Point getPoint() {
return mPoint;
}
public Vector getVector() {
return mVector;
}
}
射线与平面交点
要计算这个交点,需要计算出射线的向量要缩放多少才能刚好与平面相接触,这就是缩放因子(scaling factor),然后用这个被缩放的向量平移射线的点来找出这个相交点
缩放因子
一个平面的法向向量有无数个,比如 n1向量: (0,1,0) 、 n2向量:(0,2,0) 都是x-z平面的法向量,和平面内两个不共线的向量都垂直的向量就是法向量,例如 a向量: (1,0,0) 和 b向量:(0,0,1) ,可以发现 a 和 n1 点乘为 0,b 和 n1点乘为 0,因此 n1是 x-z 平面的法向向量,同理 n2 也是
而向量由两个点决定,例如,AB向量 = B点 - A点
前面的 n1向量可以有很多种得到的方式,例如:
A:(0,0,0) B:(0,1,0)
A:(1,0,1) B:(1,1,1)
......
从上面得到 n1向量 的方式看,n1 可以在x-z平面进行平移,区别只是得到 n1向量的方式不同
从上图可以看出,要计算缩放因子,首先创建一个向量,它在射线的起点和平面上的一个点之间,即上图中的向量 b ,缩放因子也就是:|b|/|a|
而向量a 和向量b 与平面的法向向量之间的点乘(dot product)可以计算得到,也就是说,可以通过 (a•n) / (b•n) 的方式得到缩放因子
点乘计算方式(可以用于计算两个向量之间的夹角):xa * xb + ya * yb + za * zb
几何意义(b向量在a向量方向上的投影):向量a • 向量b = |a|•|b|• cos(ab)
当射线与平面平行时,一个特殊的情况就会发生:在这种情况下,射线与平面是不可能有交点的。射线会与平面的法向向量相垂直,它们之间的点积将为0,当我们想计算缩放因子的时候,会得到一个除以0的除法
得到射线和平面的相交点
public class Geometry {
private static final String TAG = "Geometry";
......
/**
* 计算射线和平面的相交点
*
* @param ray
* @param plane
* @return
*/
public static Point intersectionPoint(Ray ray, Plane plane) {
Vector rayToPlaneVector = Vector.vectorBetween(ray.getPoint(), plane.getPoint());
Log.d(TAG, "intersectionPoint: rayToPlaneVector=" + rayToPlaneVector);
float rayToPlaneDotProduct = rayToPlaneVector.dotProduct(plane.getVector());
float dotProduct = ray.getVector().dotProduct(plane.getVector());
Log.d(TAG, "intersectionPoint: rayToPlaneDotProduct=" + rayToPlaneDotProduct + " =" + dotProduct);
float scaleFactor = rayToPlaneDotProduct / dotProduct;
Log.d(TAG, "intersectionPoint: scaleFactor=" + scaleFactor);
Point intersectionPoint = ray.getPoint().translate(ray.getVector().scale(scaleFactor));
return intersectionPoint;
}
}
移动物体
移动物体并让物体保持在边界内
// 木槌的前后左右边界值
private static final float LEFT_BOUNDS = -0.5f;
private static final float RIGHT_BOUNDS = 0.5f;
private static final float FAR_BOUNDS = -0.8f;
private static final float NEAR_BOUNDS = 0.8f;
......
/**
* 处理拖拽事件
*
* @param normalizedX
* @param normalizedY
*/
@Override
public void handleDrag(float normalizedX, float normalizedY) {
if (mIsPressed) {
// 把被触碰的点投射到一条射线上
Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
Log.d(TAG, "handleDrag: ray" + ray);
Plane plane = new Plane(new Point(0f, 0f, 0f), new Vector(0f, 1f, 0f));
// 射线与桌子平面的交点
Point touchPoint = Geometry.intersectionPoint(ray, plane);
Log.d(TAG, "handleDrag: touchPoint=" + touchPoint);
// 约束物体位置,使其保持在边界内
malletPosition = new Point(
clamp(touchPoint.getX(), LEFT_BOUNDS + mMallet.getRadius(), RIGHT_BOUNDS - mMallet.getRadius()),
touchPoint.getY(),
clamp(touchPoint.getZ(), 0 + mMallet.getRadius(), NEAR_BOUNDS - mMallet.getRadius())
);
// 碰到了冰球
float distance = Vector.vectorBetween(malletPosition, mPuckPosition).length();
// 如果碰到了冰球,计算其移动的方向
if (distance < (mMallet.getRadius() + mPuck.getRadius())) {
mPuckVector = Vector.vectorBetween(previousMalletPosition, malletPosition);
}
previousMalletPosition = malletPosition;
}
}
/**
* 将value值控制在min和max之间
*
* @param value
* @param min
* @param max
* @return
*/
private float clamp(float value, float min, float max) {
return Math.min(max, Math.max(value, min));
}
给冰球增加速度、方向和边界反射
@Override
public void onDrawFrame(GL10 gl10) {
glClear(GL_COLOR_BUFFER_BIT);
positionTableOnScreen();
mTextureProgram.useProgram();
mTextureProgram.setUniforms(modelViewProjectionMatrix, mTextureId);
mTable.bindData(mTextureProgram);
mTable.draw();
// 使用拖拽计算得到的木槌位置
positionObjectOnScreen(malletPosition.getX(), malletPosition.getY(), malletPosition.getZ());
mColorProgram.useProgram();
mColorProgram.setUniforms(modelViewProjectionMatrix, 0f, 1f, 0f);
mMallet.bindData(mColorProgram);
mMallet.draw();
// 控制冰球的范围和速度
if (mPuckPosition.getX() <= LEFT_BOUNDS + mPuck.getRadius() || mPuckPosition.getX() >= RIGHT_BOUNDS - mPuck.getRadius()) {
mPuckVector = new Vector(-mPuckVector.getX(), mPuckVector.getY(), mPuckVector.getZ());
mPuckVector = mPuckVector.scale(0.8f);
}
if (mPuckPosition.getZ() <= FAR_BOUNDS + mPuck.getRadius() || mPuckPosition.getZ() >= NEAR_BOUNDS - mPuck.getRadius()) {
mPuckVector = new Vector(mPuckVector.getX(), mPuckVector.getY(), -mPuckVector.getZ());
mPuckVector = mPuckVector.scale(0.8f);
}
// 将冰球向它的速度方向移动一点距离
mPuckPosition = mPuckPosition.translate(mPuckVector);
// 使用拖拽计算得到的冰球位置
positionObjectOnScreen(mPuckPosition.getX(), mPuckPosition.getY(), mPuckPosition.getZ());
mColorProgram.useProgram();
mColorProgram.setUniforms(modelViewProjectionMatrix, 1f, 0f, 0f);
mPuck.bindData(mColorProgram);
mPuck.draw();
// 渐渐缩小冰球移动的速度
mPuckVector = mPuckVector.scale(0.99f);
}