Open Gl 2D 中用户触摸对象的光线拾取 Android

Ray Picking for user Touching object in Open Gl 2D Android

我一直在尝试让我的游戏能够处理 OpenGl 中的用户交互。我有一个圆圈,我希望能够检测到用户的按下。我知道我需要使用 Ray Picking,我已经按照这本书 OpenGl 2 for Android 并按照教程创建了一个空气曲棍球游戏在 3D space 中检测用户点击事件。

现在我想利用我的知识从一个简单的 2D 游戏开始,但是遵循教程并应用我当前的知识并不能帮助我检测到用户点击我的对象。

我假设我做错了什么,并且 3D 和 2D 之间存在细微差别。

我有一个几何体class如下:

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

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

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

       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 Ray {
       public final Point point;
       public final Vector vector;

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

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

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

我有一个surface View onTouchEvent重写如下:

if(event.getAction() == MotionEvent.ACTION_DOWN){

    if (event != null) {

        final float normalizedX =
                        (event.getX() / (float) getWidth()) * 2 - 1;
        final float normalizedY =
                        -((event.getY() / (float) getHeight()) * 2 - 1);

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            queueEvent(new Runnable() {
                @Override
                    public void run() {
                        mRenderer.handleTouchPress(
                            normalizedX, normalizedY);
                        }
                    });

                }

                return true;
            } else {
                return false;
       }
}

最后我有一个渲染器如下:

public class ImpulseRushRenderer implements Renderer {
    private Geometry.Point circlePosition;
    private boolean circlePressed= false;
    private final Context context;
    public Circle circle;
    private float mMatrix[] = new float[16];
    private float[] mTempMatrix = new float[16];
    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];
    private final float[] mRotationMatrix = new float[16];
    private final float[] mMVPMatrix = new float[16];
    private final float[] mProjMatrix = new float[16];
    private final float[] mVMatrix = new float[16];
    private final float[] mModelMatrix = new float[16];
    private final float[] tempMatrix = new float[16];
    private final float[] invertedViewProjectionMatrix = new float[16];

    public ImpulseRushRenderer(Context context) {
        this.context = context;
    }

    LayoutInflater mInflater;

    @Override
    public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
        glClearColor(0.1725490196078431f, 0.2431372549019608f, 0.3137254901960784f, 1.0f);

        circle= new Circle();
        mInflater = (LayoutInflater)    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public void onSurfaceChanged(GL10 glUnused, int width, int height) {
        glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 glUnused) {

        glClear(GL_COLOR_BUFFER_BIT);

        Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

        Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);

        mTempMatrix = mModelMatrix.clone();
        Matrix.multiplyMM(mModelMatrix, 0, mTempMatrix, 0, mRotationMatrix, 0);

        mTempMatrix = mMVPMatrix.clone();
        Matrix.multiplyMM(mMVPMatrix, 0, mTempMatrix, 0, mModelMatrix, 0);
        Matrix.orthoM(mMatrix, 0, -1, 1, -1, 1, -1, 1);

        Matrix.orthoM(mMatrix, 0, -1, 1, -1, 1, -1, 1);

        Matrix.setIdentityM(mModelMatrix, 0);

        circle.draw(mModelMatrix);

        circlePosition= new Geometry.Point(0f, 100 / 2f, 0.4f);
    }

    private 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];

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

        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 Ray(nearPointRay,
            Geometry.vectorBetween(nearPointRay, farPointRay));
    }

    public static int loadShader(int type, String shaderCode) {

        int shader = GLES20.glCreateShader(type);

        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);

        return shader;
    }

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

        Geometry.Sphere circleBoundingSphere = new Geometry.Sphere(new Geometry.Point(
            circlePosition.x,
            circlePosition.y,
            circlePosition.z),
            100 / 2f);

        circlePressed= Geometry.intersects(circleBoundingSphere , ray);

        if (circlePressed) {
            Log.i("circlePressed", "circle was pressed");
        }
    }


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

}

据我目前所知,根据我链接的那本书中的教程,这应该是正确的。

在 "private Ray convertNormalized2DPointToRay" 中, 您可以尝试将 1 放入 nearPointWorld[3] 和 farPointWorld[3](1 放入 W),否则乘法运算符将无法正常工作。

编辑

方法一:距离函数

假设你已经有了触摸位置,物体的中心在同一space和它的半径。

int touchX, touchY;
int centerObjX, centerObjY;
int rayon;

int distX = centerObjX - touchX; // the sens don't matter
int distY = centerObjY - touchY; 
if (distX*distX + distY*distY<rayon*rayon) // squared distance
{
    // touch in the circle
}

注意 : 可以通过modelview乘中心来得到随相机移动的中心。 这种方法很好,因为它是数学的,不需要使用 GPU。 你可以有一些3D距离函数以及如何使用here

方法二:颜色选择(算法)

  1. 清除背景为黑色 (rgb=(0,0,0))
  2. 将颜色对象设置为 rgb=(1,0,0)(现在是对象的颜色 ID)
  3. 使用 glReadPixels() 选择颜色(例如)
  4. 然后比较检索到的颜色值以了解它是否是您的对象。

当场景中有很多对象或对象很复杂时,此方法非常有用。

你的方法也很棒,但结果只是你点击的位置(或方向相机->你点击的地方)。然后,还是3D用比较好