OpenGL

OpenGl ES 2.0 Learn For Android(

2019-04-21  本文已影响0人  月止风溟

OpenGl ES 2.0 Learn For Android(5)碰撞检测

19年过去了三分之一。这三分之一想了些关于人生,未来,职业方向的事情,其实没有想得太清楚。

大概的想法,可能就像下面这样吧。

“那一天我二十一岁,在我一生的黄金时代,我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云,后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消逝,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。”

前五篇的内容在这里。
OpenGl ES 2.0 Learn For Android
OpenGl ES 2.0 Learn For Android(一)世界是三角形的
OpenGl ES 2.0 Learn For Android(二)给三角形贴上图片
OpenGl ES 2.0 Learn For Android(三)初探三维的世界
OpenGl ES 2.0 Learn For Android(四)画一个三角锥

1. 相交检测

在现实生活里,碰撞是怎样发生的呢?是两个实体的边缘相接触。在OpenGL的视图世界里,肯定也是这个样子。但是我们不可能把一个物体的所有点都描述出来,然后在另一个集合里看是否有点的存在。
那么最简单的方式,就是将一个实体看做一个球,它的假设它的外边缘都在半径里。那么这个问题就简化成了,这个半径范围内,是否会有其他物体存在。
那我们在OpenGL世界坐标系里放置半径R1一个物体。如果是世界坐标系里放半径R2另一物体,那么,只要两个物体的中心位置距离dis小于两个物体半径和(R1+R2),就可以认为它们相交。这个是很容易理解的。
《OpenGL ES应用开发实践指南:Android卷》则用手指触点检测相交。其实这个问题并没有发生变化。我们还是在世界坐标系里看相交就好了。投射到屏幕的一个点,同样可以转换为世界坐标系里的两个点。这里涉及一些奇怪的技巧。比如,将一个不知道Z轴位置的点设置为-1/1。

2. Demo演示

这边我会做一个更简易的版本。我在世界坐标系里放置一个边长为1的正方体。如下图所示。


正方体

在之前讲到三角形的画法的时候,有讲到三角带。这是openGL为了节省节点,设计的绘制方案。这里的话,顶点也按对应的方式进行排布。
为了方便,我并没有绘制它的上下两个面。
将上一篇的顶点下面方式赋值。颜色也对应改改。

   private float[] mTrianglePoints =
                   { 0.5f,  0.5f,  0.5f,
                     0.5f, -0.5f,  0.5f,
                     0.5f,  0.5f, -0.5f,
                     0.5f, -0.5f, -0.5f,
                    -0.5f,  0.5f, -0.5f,
                    -0.5f, -0.5f, -0.5f,
                    -0.5f,  0.5f,  0.5f,
                    -0.5f, -0.5f,  0.5f,
                     0.5f,  0.5f,  0.5f,
                     0.5f, -0.5f,  0.5f};

在上一篇的时候,我只用了modelMatrix和projectionMatrix。因为上一篇使用的默认视角,也就是从Z轴往它的负方向看去。
其实这一篇方向还是不会发生变化,但是这里需要用到视角的概念,也就是setLookAtM()这个方法。

Matrix.setLookAtM(mVMatrix, 0,
                0f, 0f, 5f,            //相机坐标
                0f, 0f, 0f,            //目标坐标
                0.0f, 1.0f, 0.0f);     //相机正上方向量

所以引入这三个矩阵

    private final float[] viewMatrix = new float[16];//视角(摄像机位置)矩阵
    private final float[] viewProjectionMatrix = new float[16];
    private final float[] invertedViewProjectionMatrix = new float[16];

viewMatrix用来保存相机矩阵。viewProjectionMatrix用来保存透视投影和相机矩阵的乘积。invertedViewProjectionMatrix用来保存viewProjectionMatrix的逆矩阵。

因为要拿到触碰事件,给GlSurfaceView.setOnTouchListener

     glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if(motionEvent!=null){
                    final float normalizedX =
                            (motionEvent.getX()/(float)view.getWidth())*2-1;
                    final float normalizedY =
                            -((motionEvent.getY()/(float)view.getHeight())*2-1);
                    if(motionEvent.getAction()==MotionEvent.ACTION_DOWN){
                        glSurfaceView.queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                renderer.handleTouchDown(normalizedX,normalizedY);
                            }
                        });
                        return true;
                    }else if(motionEvent.getAction()==MotionEvent.ACTION_BUTTON_PRESS){
                        glSurfaceView.queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                renderer.handleTouchPress(normalizedX,normalizedX);
                            }
                        });
                        return true;
                    }else if(motionEvent.getAction()==MotionEvent.ACTION_UP){
                        glSurfaceView.queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                renderer.handleTouchUp(normalizedX,normalizedX);
                            }
                        });
                        return true;
                    }
                    return false;
                }
                return false;
            }
        });

有意思的来了。我们知道,Android View的坐标系是左上角为原点,往右是x正方向,往下是y正方向。但是OpenGL ES归一化坐标里,坐标原点在屏幕正中央,往右是x正方向,往上是y正方向。 以X轴为例,要做是的从[0,1]映射到[-1,1]。那么x*2-1就可以达到想要的效果。这个如果后面有时间介绍构型函数,会见得很多。

我们拿到了触点的归一化坐标,我们要转换成世界坐标,这样才能做距离计算。
计算距离的工具类直接从示例中拿出来

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

        @Override
        public String toString() {
            return "Point{" +
                    "x=" + x +
                    ", y=" + y +
                    ", z=" + 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
                );
        }
        
        // 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);
        }

        @Override
        public String toString() {
            return "Vector{" +
                    "x=" + x +
                    ", y=" + y +
                    ", z=" + z +
                    '}';
        }
    }
    
    public static class Ray {
        public final Point point;
        public final Vector vector;

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

        @Override
        public String toString() {
            return "Ray{" +
                    "point=" + point +
                    ", vector=" + vector +
                    '}';
        }
    }
    
    // TODO: Re-use shared stuff in classes as an exercise

    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;
    }
    
    // http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
    // Note that this formula treats Ray as if it extended infinitely past
    // either point.
    public static float distanceBetween(Point point, Ray ray) {
        Vector p1ToPoint = vectorBetween(ray.point, point);
        Vector p2ToPoint = vectorBetween(ray.point.translate(ray.vector), point);

        // The length of the cross product gives the area of an imaginary
        // parallelogram having the two vectors as sides. A parallelogram can be
        // thought of as consisting of two triangles, so this is the same as
        // twice the area of the triangle defined by the two vectors.
        // http://en.wikipedia.org/wiki/Cross_product#Geometric_meaning
        float areaOfTriangleTimesTwo = p1ToPoint.crossProduct(p2ToPoint).length();
        float lengthOfBase = ray.vector.length();

        // The area of a triangle is also equal to (base * height) / 2. In
        // other words, the height is equal to (area * 2) / base. The height
        // of this triangle is the distance from the point to the ray.
        float distanceFromPointToRay = areaOfTriangleTimesTwo / lengthOfBase;
        return distanceFromPointToRay;
    }
        
    // http://en.wikipedia.org/wiki/Line-plane_intersection
    // This also treats rays as if they were infinite. It will return a
    // point full of NaNs if there is no intersection point.
    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;
    }
}

这里光使用计算距离的方法,可以看书的9.2章。我觉得我肯定是没作者讲得好了。

使用上一篇用的投影参数,矩阵显得有些大了,所以在使用前,先把他们都缩小到1/5大小。

这里的话我要偷些懒,把handleTouchPress直接用上工具类里的方法。

    public void handleTouchPress(float normalizedX, float normalizedY) {
        Log.e(TAG, "normalizedX " + normalizedX + " normalizedY " + normalizedY);
        Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
        Log.e(TAG, "ray " + ray.toString());

        // Now test if this ray intersects with the mallet by creating a
        // bounding sphere that wraps the mallet.
        Geometry.Sphere malletBoundingSphere = new Geometry.Sphere(new Geometry.Point(
                0F,
                0F,
                0F),
                0.1F);

        // If the ray intersects (if the user touched a part of the screen that
        // intersects the mallet's bounding sphere), then set malletPressed =
        // true.
        mIsPressed = Geometry.intersects(malletBoundingSphere, ray);
    }

在将点击的坐标转换为世界坐标里线段的方法里,有用到invertedViewProjectionMatrix

    private Geometry.Ray    convertNormalized2DPointToRay(
            float normalizedX, float normalizedY) {
        // We'll convert these normalized device coordinates into world-space
        // coordinates. We'll pick a point on the near and far planes, and draw a
        // line between them. To do this transform, we need to first multiply by
        // the inverse matrix, and then we need to undo the perspective divide.
        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);

        // Why are we dividing by W? We multiplied our vector by an inverse
        // matrix, so the W value that we end up is actually the *inverse* of
        // what the projection matrix would create. By dividing all 3 components
        // by W, we effectively undo the hardware perspective divide.
        divideByW(nearPointWorld);
        divideByW(farPointWorld);

        // We don't care about the W value anymore, because our points are now
        // in world coordinates.
        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));
    }

它是viewProjectionMatrix的逆矩阵

  invertM(invertedViewProjectionMatrix, 0, viewProjectionMatrix, 0);

那我做的判断是,如果点击了正方体,则停止旋转。效果如下


点击停止旋转

demo地址:
https://github.com/YueZhiFengMing/LearnOpenGl/tree/master/Fourth3D

参考资料

  1. 《OpenGL ES应用开发实践指南:Android卷》
上一篇下一篇

猜你喜欢

热点阅读