javaandroid-studiomatrixopengl-esray-picking

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


I have been trying get my game to be able to handle a user interaction in OpenGl. I have a circle and I would like to be able to detect a press by the user. I know I need to use Ray Picking, I have followed this book OpenGl 2 for Android and have followed the tutorial to create a air hockey game in 3D space and detect the users click event.

Now I wish to use my knowledge to start simple with a 2D game, However following the tutorial and applying my current knowledge is not helping me achieve to be able to detect a user clicking my object.

I am assuming I am doing something wrong and there is a slight difference between doing it for 3D and 2D.

I have a geometry class as follows:

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

I have a surface View onTouchEvent Overridden as follows:

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

Lastly I have a renderer as Follows:

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

}

To my current knowledge and from the tutorial in that book I linked this should would correctly.


Solution

  • in "private Ray convertNormalized2DPointToRay" , Can you try to put 1 to nearPointWorld[3] and farPointWorld[3] (1 to W), because the multiply operator won't work correctly otherwise.

    EDIT

    Method 1 : distance function

    Assuming you already have the touch position, the center of the object in the same space and its radius.

    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
    }
    

    Note : You can get the center with the camera move by multiply center by modelview. This method is nice because it's mathematical and does not require GPU use. You can have some 3D distance function and how to use here

    Methode 2 : Color picking (algorithm)

    1. Clear the background to black (rgb=(0,0,0))
    2. Set the color object to rgb=(1,0,0) (it's now the color id of the object)
    3. pick color using glReadPixels() (for example)
    4. then compare the retrieved color value to know if it's your object or not.

    This method is great when you have lots object in the scene or if the object is complex.

    Your method is great too but the result is only a position of where you clicked (or a direction camera->where you clicked). Then, it's better for 3D use.