Android OpenGL触摸反馈

Android OpenGL 触摸交互实现
本文介绍如何在Android平台上利用OpenGL ES实现触摸交互功能,包括触摸事件处理、触摸点到三维射线的转换、物体的移动等关键技术点。

Android OpenGL触摸反馈

首先申明下,本文为笔者学习《OpenGL ES应用开发实践指南》的笔记,并加入笔者自己的理解和归纳总结。

1、添加触摸支持

通过调用setOnTouchListener方法监听视图的触控事件,然后把触控位置转化为[-1, 1]的归一化设备坐标,最后转发给渲染器。
private OpenGLTouchShaderRender mTouchShaderRender;

mSurfaceView.setOnTouchListener(new View.OnTouchListener() {
	@Override
	public boolean onTouch(View v, MotionEvent event) {
		if (event != null) {
			final float normalizedX = (event.getX() / (float) v.getWidth()) * 2 - 1;
			final float normalizedY = -((event.getY() / (float) v.getHeight()) * 2 - 1);

			if (event.getAction() == MotionEvent.ACTION_DOWN) {
				mSurfaceView.queueEvent(new Runnable() {
					@Override
					public void run() {
						if (mTouchShaderRender != null)
							mTouchShaderRender.handleTouchPress(normalizedX, normalizedY);
					}
				});
			} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
				mSurfaceView.queueEvent(new Runnable() {
					@Override
					public void run() {
						if (mTouchShaderRender != null)
							mTouchShaderRender.handleTouchDrag(normalizedX, normalizedY);
					}
				});
			}
			return true;
		}
		return false;
	}
}); 

2、二维点扩展成三维直线

当我们把一个三维场景投递到二维屏幕的时候,我们使用透视投影和透视除法把顶点坐标变换为归一化设备坐标。
现在我们有被触摸点的归一化设备坐标,为了把为触摸点转换为一个三维射线,我们需要取消透视投影和透视除法。

private final float[] invertedViewProjectMatrix = new float[16];
Matrix.invertM(invertedViewProjectMatrix, 0, viewProjectMatrix, 0);
为了把被触摸点映射到一条射线,我们在归一化设备坐标里设置了两个点:其中一个点是z值为-1,而另一个点是z值为1的点。
private Geometry.Ray convertNormalized2DPointToRay(float normalizedX, float normalizedY) {
	final float[] nearPointNdc = {normalizedX, normalizedY, -1, 1};
	final float[] farPointNdc = {normalizedX, normalizedY, 1, 1};

	final float[] nearPointWorld = new float[4];
	final float[] farPointWorld = new float[4];
	
	Matrix.multiplyMV(nearPointWorld, 0, invertedViewProjectMatrix, 0, nearPointNdc, 0);
	Matrix.multiplyMV(farPointWorld, 0, invertedViewProjectMatrix, 0, farPointNdc, 0);

	// 把x, y, z除以这些反转的w,这样就撤销了透视除法的影响
	divideByW(nearPointWorld);
	divideByW(farPointWorld);

	Geometry.Point nearPointRay =
			new Geometry.Point(nearPointWorld[0], nearPointWorld[1], nearPointWorld[2]);
	Geometry.Point farPointRay =
			new Geometry.Point(farPointWorld[0], farPointWorld[1], farPointWorld[2]);

	// 返回两点之间的射线
	return new Geometry.Ray(nearPointRay,
			Geometry.vectorBetween(nearPointRay, farPointRay));
}

private void divideByW(float[] vector) {
	vector[0] /= vector[3];
	vector[1] /= vector[3];
	vector[2] /= vector[3];
}
在Geometry中定义射线类Ray,矢量类Vector
public static class Ray {
	public final Point point;
	public final Vector vector;

	public Ray(Point point, Vector vector) {
		this.point = point;
		this.vector = vector;
	}
}

public static class Vector  {
	public final float x, y, z;

	public Vector(float x, float y, float z) {
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
}

public static Vector vectorBetween(Point from, Point to) {
	return new Vector(to.x - from.x, to.y - from.y, to.z - from.z);
}

3、相交测试

我们假定木锥是一个球体,这样相交测试就会相当容易。
在onSurfaceCreated中定义蓝色木锥初始位置
blueMalletPoint = new Geometry.Point(0, -0.4f, mMallet.height / 2);
定义一个球体,判断是否与射线相交
Geometry.Sphere malletBoundingSphere = new Geometry.Sphere(blueMalletPoint, mMallet.height/2);
malletPressed = Geometry.intersects(malletBoundingSphere, ray);
主要方法是计算圆心到射线之间的距离,和圆的半径比较。而这个距离是三角形的面积*2/射线长度。里面的数学原理笔者也不是很明白,只能照搬过来。

public static boolean intersects(Sphere sphere, Ray ray) {
	return distanceBetween(sphere.center, ray) < sphere.radius;
}

private static float distanceBetween(Point point, Ray ray) {
	Vector p1ToPoint = vectorBetween(ray.point, point);
	Vector p2ToPoint = vectorBetween(ray.point.translate(ray.vector), point);

	float areaOfTriangleTimesTwo = p1ToPoint.crossProduct(p2ToPoint).length();
	float lengthOfBase = ray.vector.length();

	float distanceFromPointToRay = areaOfTriangleTimesTwo / lengthOfBase;

	return distanceFromPointToRay;
}
Vector类
public static class Vector  {

	public float length() {
		return (float)Math.sqrt(
				x * x  + y * y + z * z);
	}

	public Vector crossProduct(Vector other) {
		return new Vector(
				(y * other.z) - (z * other.y),
				(z * other.x) - (x * other.z),
				(x * other.y) - (y * other.x));
	}

}

4、移动物体

首先需要手指按住木锥,然后把拖动的点转换成射线,找到这条射线和桌子的相交点,最后把木锥移动到那个点。
public void handleTouchDrag(float normalizedX, float normalizedY) {
	if (malletPressed) {
		Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);

		// 平面位于(0, 0, 0),有一个法向向量(0, 0, 1)
		Geometry.Plane plane = new Geometry.Plane(new Geometry.Point(0, 0, 0),
				new Geometry.Vector(0, 0, 1));
		Geometry.Point touchedPoint = Geometry.intersectionPoint(ray, plane);
		blueMalletPoint = new Geometry.Point(touchedPoint.x, touchedPoint.y,
				mMallet.height/2f);
	}
}
Plane类,定义了一个平面,它包含一个法向向量和平面上一个点
public static class Plane {
	public final Point point;
	public final Vector normal;

	public Plane(Point point, Vector normal) {
		this.point = point;
		this.normal = normal;
	}
}
intersectionPoint计算相交点,里面的数学原理笔者也不是很明白,只能照搬过来。
public static Point intersectionPoint(Ray ray, Plane plane) {
	Vector rayToPlaneVector = vectorBetween(ray.point, plane.point);

	float scaleFactor = rayToPlaneVector.dotProduct(plane.normal)
			/ ray.vector.dotProduct(plane.normal);

	Point intersectionPoint = ray.point.translate(ray.vector.scale(scaleFactor));
	return intersectionPoint;
}
Vector类
public static class Vector  {

	public float dotProduct(Vector other) {
		return x * other.x + y * other.y + z * other.z;
	}

	public Vector scale(float f) {
		return new Vector(x * f, y * f, z * f);
	}
}

5、OpenGLTouchShaderRender类

class OpenGLTouchShaderRender implements GLSurfaceView.Renderer {
	private boolean malletPressed = false;
	private Geometry.Point blueMalletPoint;

	private final float[] invertedViewProjectionMatrix = new float[16];

	private final float[] projectionMatrix = new float[16];
	private final float[] modelMatrix = new float[16];
	private final float[] viewMatrix = new float[16];
	private final float[] viewProjectionMatrix = new float[16];
	private final float[] modelViewProjectionMatrix = new float[16];

	private TextureProgram mTextureProgram;
	private GeometryColorProgram mColorProgram;

	private Table mTable;
	private GeometryMallet mMallet;

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

		mTable = new Table();

		// 创建木锥
		ResourceDataBuilder.ResourceData data = ResourceDataBuilder.createMallet(
				new Geometry.Cylinder(new Geometry.Point(0f, 0f, 0f), 0.08f, 0.15f), 32);
		mMallet = new GeometryMallet(data, 0.08f, 0.15f);
		blueMalletPoint = new Geometry.Point(0, -0.4f, mMallet.height / 2);

		mTextureProgram = new TextureProgram(OpenGLTouchShaderActivity.this,
				R.drawable.air_hockey_surface);
		mColorProgram = new GeometryColorProgram(OpenGLTouchShaderActivity.this);
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		GLES20.glViewport(0, 0, width, height);

		// 创建透视投影
		Matrix.perspectiveM(projectionMatrix, 0, 45, (float)width / (float)height, 1, 10);
		Matrix.setLookAtM(viewMatrix, 0, 0f, -2.4f, 1.4f, 0f, 0f, 0f, 0f, 1f, 0f);

		Matrix.multiplyMM(viewProjectionMatrix, 0, projectionMatrix, 0, viewMatrix, 0);
		Matrix.invertM(invertedViewProjectionMatrix, 0, viewProjectionMatrix, 0);
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

		// 绘制桌子
		positionTableInScene();
		mTextureProgram.setUniform(modelViewProjectionMatrix);
		mTable.bindData(mTextureProgram);
		mTable.draw();

		// 绘制蓝色木锥
		positionObjectInScene(blueMalletPoint.x, blueMalletPoint.y,  blueMalletPoint.z);
		mColorProgram.setUniform(modelViewProjectionMatrix);
		mColorProgram.setColor(0f, 0f, 1f);
		mMallet.bindData(mColorProgram);
		mMallet.draw();
	}

	private void positionTableInScene() {
		Matrix.setIdentityM(modelMatrix, 0);
		Matrix.multiplyMM(modelViewProjectionMatrix, 0, viewProjectionMatrix,
				0, modelMatrix, 0);
	}

	// 移动木锥
	private void positionObjectInScene(float x, float y, float z) {
		Matrix.setIdentityM(modelMatrix, 0);
		Matrix.translateM(modelMatrix, 0, x, y, z);
		Matrix.multiplyMM(modelViewProjectionMatrix, 0, viewProjectionMatrix,
				0, modelMatrix, 0);
	}

	public void handleTouchPress(float normalizedX, float normalizedY) {
		Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
		// 定义一个球体,判断是否与射线相交
		Geometry.Sphere malletBoundingSphere = new Geometry.Sphere(blueMalletPoint, mMallet.height/2);
		malletPressed = Geometry.intersects(malletBoundingSphere, ray);
		LogUtil.log("OpenGLTouchShaderRender", "pressed: " + malletPressed);
	}

	public void handleTouchDrag(float normalizedX, float normalizedY) {
		if (malletPressed) {
			Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);

			Geometry.Plane plane = new Geometry.Plane(new Geometry.Point(0, 0, 0),
					new Geometry.Vector(0, 0, 1));
			Geometry.Point touchedPoint = Geometry.intersectionPoint(ray, plane);
			blueMalletPoint = new Geometry.Point(touchedPoint.x, touchedPoint.y,
					mMallet.height/2f);
		}
	}

	private Geometry.Ray convertNormalized2DPointToRay(float normalizedX, float normalizedY) {
		LogUtil.log("OpenGLTouchShaderRender", "normalizedX: " + normalizedX);
		LogUtil.log("OpenGLTouchShaderRender", "normalizedY: " + normalizedY);
		final float[] nearPointNdc = {normalizedX, normalizedY, -1, 1};
		final float[] farPointNdc = {normalizedX, normalizedY, 1, 1};

		final float[] nearPointWorld = new float[4];
		final float[] farPointWorld = new float[4];

		Matrix.multiplyMV(nearPointWorld, 0, invertedViewProjectionMatrix, 0, nearPointNdc, 0);
		Matrix.multiplyMV(farPointWorld, 0, invertedViewProjectionMatrix, 0, farPointNdc, 0);

		// 把x, y, z除以这些反转的w,这样就撤销了透视除法的影响
		divideByW(nearPointWorld);
		divideByW(farPointWorld);

		Geometry.Point nearPointRay =
				new Geometry.Point(nearPointWorld[0], nearPointWorld[1], nearPointWorld[2]);
		Geometry.Point farPointRay =
				new Geometry.Point(farPointWorld[0], farPointWorld[1], farPointWorld[2]);

		// 返回两点之间的射线
		return new Geometry.Ray(nearPointRay,
				Geometry.vectorBetween(nearPointRay, farPointRay));

	}

	private void divideByW(float[] vector) {
		vector[0] /= vector[3];
		vector[1] /= vector[3];
		vector[2] /= vector[3];
	}

}

6、Geometry类

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;
        }

        // Z轴平移
        public Point translateZ(float distance) {
            return new Point(x, y, z + distance);
        }

        // 添加translate方法
        public Point translate(Vector vector) {
            return new Point(x + vector.x, y + vector.y, z + vector.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;
        }
    }

    public static class Ray {
        public final Point point;
        public final Vector vector;

        public Ray(Point point, Vector vector) {
            this.point = point;
            this.vector = vector;
        }
    }

    public static class Vector  {
        public final float x, y, z;

        public Vector(float x, float y, float z) {
            this.x = x;
            this.y = y;
            this.z = z;
        }

        public float length() {
            return (float)Math.sqrt(
                    x * x  + y * y + z * z);
        }

        // http://en.wikipedia.org/wiki/Cross_product
        public Vector crossProduct(Vector other) {
            return new Vector(
                    (y * other.z) - (z * other.y),
                    (z * other.x) - (x * other.z),
                    (x * other.y) - (y * other.x));
        }

        // http://en.wikipedia.org/wiki/Dot_product
        public float dotProduct(Vector other) {
            return x * other.x + y * other.y + z * other.z;
        }

        public Vector scale(float f) {
            return new Vector(x * f, y * f, z * f);
        }
    }

    public static class Sphere {
        public final Point center;
        public final float radius;

        public Sphere(Point center, float radius) {
            this.center = center;
            this.radius = radius;
        }
    }

    public static class Plane {
        public final Point point;
        public final Vector normal;

        public Plane(Point point, Vector normal) {
            this.point = point;
            this.normal = normal;
        }
    }

    public static Vector vectorBetween(Point from, Point to) {
        return new Vector(to.x - from.x, to.y - from.y, to.z - from.z);
    }

    public static boolean intersects(Sphere sphere, Ray ray) {
        return distanceBetween(sphere.center, ray) < sphere.radius;
    }

    private static float distanceBetween(Point point, Ray ray) {
        Vector p1ToPoint = vectorBetween(ray.point, point);
        Vector p2ToPoint = vectorBetween(ray.point.translate(ray.vector), point);

        float areaOfTriangleTimesTwo = p1ToPoint.crossProduct(p2ToPoint).length();
        float lengthOfBase = ray.vector.length();

        float distanceFromPointToRay = areaOfTriangleTimesTwo / lengthOfBase;

        return distanceFromPointToRay;
    }

    public static Point intersectionPoint(Ray ray, Plane plane) {
        Vector rayToPlaneVector = vectorBetween(ray.point, plane.point);

        float scaleFactor = rayToPlaneVector.dotProduct(plane.normal)
                / ray.vector.dotProduct(plane.normal);

        Point intersectionPoint = ray.point.translate(ray.vector.scale(scaleFactor));
        return intersectionPoint;
    }

}
显示如下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值