Android OpenGL ES ray picking

Вы разрабатываете приложение под Android и вам нужно узнать, на какой объект пользователь ткнул пальцем.

Самый простой вариант — отрисовать всю сцену в отдельный буфер, чтобы каждый объект был своим цветом и посмотреть какой цвет будет в нужной точке. Его отринем, как несовершенный — нужно лишний раз отрисовывать всю геометрию и на относительно слабом процессоре мобильных устройств это аукнется.

Вариант посложнее — ray picking. Строится луч, идущий из камеры, и проверяются его пересечения с объектами сцены. Практически сразу вы найдёте в гугле вот этот туториал, но выясните, что он не работает. Точнее, координаты даёт, но не те. Потом найдёте вот это. Хоть и для айфона, но принцип вроде одинаковый? Одинаковый. Результат будет в точности как в предыдущем варианте.

А на самом деле всё очень просто.

В любом случае сначала отсюда нужно будет взять классы MatrixStack, MatrixGrabber и MatrixTrackingGL и подключить именно так, как описано в первой статье:

private MatrixGrabber mg = new MatrixGrabber();
public void onCreate(Bundle savedInstanceState)
{
	super.onCreate(savedInstanceState);
	glSurface.setGLWrapper(new GLSurfaceView.GLWrapper()
	{
		public GL wrap(GL gl)
		{
			return new MatrixTrackingGL(gl);
		}
	});
}

Где-то внутри ваша функция отрисовки наверняка задаёт Projection и ModelView матрицы:

gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, fFOV, fAspectRatio, fNear, fFar);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
GLU.gluLookAt(gl, posCamera[0], posCamera[1], posCamera[2], posLook[0], posLook[1], posLook[2], 0, 0, 1);

Сразу после установки Projection и ModelView матриц нужно будет сохранить их, вызвав mg.getCurrentState (gl):

// store projection and modelview matrices
mg.getCurrentState(gl);

Теперь построим луч, который выходит из камеры и проходит через нажатую на экране точку.

И вот теперь уже начинается интересное. Для этого воспользуемся сохранёнными матрицами и функцией gluUnProject:

private float[] getViewRay(float[] tap)
{
	// view port
	int[] viewport = { 0, 0, nScreenWidth, nScreenHeight };

	// far eye point
	float[] eye = new float[4];
	GLU.gluUnProject(tap[0], nScreenHeight - tap[1], 0.9f, mg.mModelView, 0, mg.mProjection, 0, viewport, 0, eye, 0);

	// fix
	if (eye[3] != 0)
	{
		eye[0] = eye[0] / eye[3];
		eye[1] = eye[1] / eye[3];
		eye[2] = eye[2] / eye[3];
	}

	// ray vector
	float[] ray = { eye[0] - posCamera[0], eye[1] - posCamera[1], eye[2] - posCamera[2], 0.0f };
	return ray;
}

В функции gluUnProject параметр winz принимает значения 0 для плоскости near и 1 для плоскости far. Значение 0.9 лежит довольно далеко в глубине видимой области, но самое важное — с ним определение координат работает правильно.

На выходе функции имеем вектор, выходящий из камеры и проходящий через нужную точку на экране. Остаётся определить, пересекается ли он с нужным объектом, например с окружающей объект сферой или кубиком, а это уже довольно простая математика.

Например, пересечение с плоскостью z = 0. Луч задаётся уравнением posCamera + ray * t и пересекает эту плоскость при t = -posCamera.z / ray.z (если ray.z = 0, то пересечения нет). Соответственно, координаты точки пересечения: {posCamera.x + ray.x * t, posCamera.y + ray.y * t, 0}.

Если объект находится в объёме xmin...xmax, ymin...ymax, zmin...zmax, то нужно будет определить точки пересечения с ограничивающими плоскостями и проверить, что они лежат в нужных границах.

Пересечение со сферой определяется чть сложнее, но принцип тот же.