OpenGL之触控反馈

OpenGL之基础
OpenGL之绘制简单形状
OpenGL之颜色
OpenGL之调整屏幕宽高比
OpenGL之三维
OpenGL之纹理
OpenGL之构建简单物体

在Activity中监听触控事件

在Activity里,可以通过调用setOnTouchListener()监听视图的触控事件,然后转发触控事件给渲染器

在Android里,触控事件是发生在视图的坐标空间中的,视图的左上角映射到(0,0),右下角映射到的坐标等于视图的大小,而在着色器中需要使用归一化设备坐标,因此我们需要把触控事件坐标转换回归一化设备坐标,并且需要把y轴反转,并把每个坐标按比例映射到范围[-1,1]内

/**
 * 处理触控事件
 */
private void dealTouchEvent(ITouchRenderer renderer) {
    glSurfaceView.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event != null) {
                // 把触控事件坐标转换回归一化设备坐标
                float normalizedX = event.getX() / v.getWidth() * 2 - 1;
                // 把y轴反转
                float normalizedY = -(event.getY() / v.getHeight() * 2 - 1);

                // 转发触控事件给渲染器,通过GLSurfaceView.queueEvent()方法给OpenGL线程分发调用
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    glSurfaceView.queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            renderer.handlePress(normalizedX, normalizedY);
                        }
                    });
                } else if (event.getAction() == MotionEvent.ACTION_MOVE) {

                    glSurfaceView.queueEvent(new Runnable() {
                        @Override
                        public void run() {
                            renderer.handleDrag(normalizedX, normalizedY);
                        }
                    });
                }
                return true;
            }
            return false;
        }
    });
}

相交测试

得到了屏幕的被触碰的归一化设备坐标,还需要使用相交测试判断被触碰的坐标是否在木槌里

相交测试步骤

  1. 把二维屏幕的归一化设备坐标转换为三维空间一条射线,要做到这点,我们要把被触碰的点投射到一条射线上,这条射线从我们的视点跨越那个三维场景
  2. 检查这条射线是否与木槌相交,为了使判断简单些,假定木槌是一个差不多同样大小的球,然后测试那个球

二维屏幕的归一化设备坐标转换为三维空间一条射线

把一个三维场景投递到二维屏幕的时候,使用透视投影和透视除法把顶点坐标变换为归一化设备坐标,为了把二维屏幕被触碰的点转换为一个三维射线(射线的近端映射到我们在投影矩阵中定义的视椎体的近平面,直线的远端映射到视椎体的远平面),需要做的就是取消透视投影和透视除法

要实现这个转换,需要一个反转的矩阵,它会取消视图矩阵和投影矩阵的效果,而 Matrix.invertM() 方法就可以获得一个矩阵的反转矩阵,方法原型

public static boolean invertM(float[] mInv, int mInvOffset, float[] m,
        int mOffset) {

这个调用会创建一个反转的矩阵,我们可以用它把那个二维被触碰的点转换为两个三维坐标。如果绘制的时候使用了视图矩阵,场景可以移来移去,它就会影响场景的哪一部分在手指下面,因此需要把视图矩阵考虑在内。通过反转视图投影矩阵实现取消透视投影和透视除法

在这之前,需要先定义点,射线和向量

public class Point {
    private float mX;
    private float mY;
    private float mZ;

    public Point(float x, float y, float z) {
        this.mX = x;
        this.mY = y;
        this.mZ = z;
    }

    public Point translate(Vector vector) {
        return new Point(
                mX += vector.getX(),
                mY += vector.getY(),
                mZ += vector.getZ()
        );
    }

    public Point translateY(float y) {
        return new Point(mX, mY + y, mZ);
    }

    public float getX() {
        return mX;
    }

    public float getY() {
        return mY;
    }

    public float getZ() {
        return mZ;
    }

    @Override
    public String toString() {
        return "Point{" +
                "mX=" + mX +
                ", mY=" + mY +
                ", mZ=" + mZ +
                '}';
    }
}

射线

public class Ray {
    private Point mPoint;
    private Vector mVector;

    public Ray(Point point, Vector vector) {
        mPoint = point;
        mVector = vector;
    }

    public Point getPoint() {
        return mPoint;
    }

    public Vector getVector() {
        return mVector;
    }

    @Override
    public String toString() {
        return "Ray{" +
                "mPoint=" + mPoint +
                ", mVector=" + mVector +
                '}';
    }
}

向量

public class Vector {
    private float mX;
    private float mY;
    private float mZ;

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

    public static Vector vectorBetween(Point from, Point to) {
        return new Vector(
                to.getX() - from.getX(),
                to.getY() - from.getY(),
                to.getZ() - from.getZ()
        );
    }

    public float getX() {
        return mX;
    }

    public float getY() {
        return mY;
    }

    public float getZ() {
        return mZ;
    }

    public Vector crossProduct(Vector other) {
        return new Vector(
                mY * other.mZ - mZ * other.mY,
                -(mX * other.mZ - mZ * other.mX),
                mX * other.mY - mY * other.mX
        );

    }

    public float length() {
        return (float) Math.sqrt(mX * mX + mY * mY + mZ * mZ);
    }

    public float dotProduct(Vector other) {
        return mX * other.mX + mY * other.mY + mZ * other.mZ;
    }

    public Vector scale(float ratio) {
        return new Vector(
                mX * ratio,
                mY * ratio,
                mZ * ratio
        );
    }

    @Override
    public String toString() {
        return "Vector{" +
                "mX=" + mX +
                ", mY=" + mY +
                ", mZ=" + mZ +
                '}';
    }
}

二维坐标转为三维射线

在 onSurfaceChanged() 方法里调用如下方法,获取视图投影矩阵的反转矩阵

Matrix.invertM(invertedViewProjectionMatrix, 0, viewProjectionMatrix, 0);
    /**
     * 把被触碰的点投射到一条射线上,这条射线从我们的视点跨越那个三维场景
     *
     * @param normalizedX
     * @param normalizedY
     * @return
     */
    private Ray convertNormalized2DPointToRay(float normalizedX, float normalizedY) {

        // 在视椎体近平面和远平面各选一个点
        float[] nearPointNdc = new float[]{normalizedX, normalizedY, 1, 1};
        float[] farPointNdc = new float[]{normalizedX, normalizedY, 10, 1};

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

//        invertedViewProjectionMatrix[0] = 0.28241837f;
        // 取消所选点的视图投影、透视投影,获得世界坐标
        Matrix.multiplyMV(nearPointWorld, 0, invertedViewProjectionMatrix, 0, nearPointNdc, 0);
        Matrix.multiplyMV(farPointWorld, 0, invertedViewProjectionMatrix, 0, farPointNdc, 0);

        // 取消透视除法的影响
        divideByW(nearPointWorld);
        divideByW(farPointWorld);
        
        // 将 float[] 转为Point
        Point nearPoint = new Point(nearPointWorld[0], nearPointWorld[1], nearPointWorld[2]);
        Point farPoint = new Point(farPointWorld[0], farPointWorld[1], farPointWorld[2]);

        return new Ray(nearPoint, Vector.vectorBetween(nearPoint, farPoint));
    }

检查这条射线是否与木槌相交

为了使判断简单些,假定木槌是一个差不多同样大小的球,然后测试那个球是否与射线相交,因此需要定义球的结构

球的结构

public class Sphere {
    private Point mCenter;
    private float mRadius;

    public Sphere(Point center, float radius) {
        mCenter = center;
        mRadius = radius;
    }

    public Point getCenter() {
        return mCenter;
    }

    public float getRadius() {
        return mRadius;
    }
}

相交测试

相交测试需要一些几何判断,如下

public class Geometry {
    private static final String TAG = "Geometry";

    /**
     * 判断球体是否与射线相交
     *
     * @param sphere
     * @param ray
     * @return
     */
    public static boolean intersects(Sphere sphere, Ray ray) {
        return distance(sphere.getCenter(), ray) < sphere.getRadius();
    }

    /**
     * 计算点到射线的距离
     *
     * @param center
     * @param ray
     * @return
     */
    private static float distance(Point center, Ray ray) {
        Vector p1ToCenter = Vector.vectorBetween(ray.getPoint(), center);
        Vector p2ToCenter = Vector.vectorBetween(ray.getPoint().translate(ray.getVector()), center);

        float areaOfTriangleTimesTwo = p1ToCenter.crossProduct(p2ToCenter).length();
        float lengthOfBase = ray.getVector().length();

        float distance = areaOfTriangleTimesTwo / lengthOfBase;
        Log.d(TAG, "distance: " + distance);
        return distance;
    }
}

在按下事件里判断触控点是否与物体相交

/**
 * 处理按下事件
 *
 * @param normalizedX
 * @param normalizedY
 */
@Override
public void handlePress(float normalizedX, float normalizedY) {
    Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
    Log.d(TAG, "handlePress: ray=" + ray);

    Sphere sphere = new Sphere(new Point(malletPosition.getX(), mMallet.getHeight() / 2f, malletPosition.getZ()), mMallet.getHeight() / 2f);
    Log.d(TAG, "handlePress: sphere.getRadius()=" + sphere.getRadius());
    mIsPressed = Geometry.intersects(sphere, ray);
    Log.d(TAG, "handlePress: mIsPressed=" + mIsPressed);
}

通过拖拽移动物体

只有当我们开始就用手指按住那个木槌时,我们才想要拖动它,因此,首先检查 malletPressed 是否为 true,如果是,那我们就做射线转换,一旦我们有了表示被触碰点的射线,我们就要找出这条射线与桌子的平面在哪里相交了,然后,把木槌移动到那个点

平面

包含一个法向向量(normal vector)和平面上的一个点,法向向量是一个垂直于那个平面的一个向量

public class Plane {
    private Point mPoint;
    private Vector mVector;

    public Plane(Point point, Vector vector) {
        mPoint = point;
        mVector = vector;
    }

    public Point getPoint() {
        return mPoint;
    }

    public Vector getVector() {
        return mVector;
    }
}

射线与平面交点

要计算这个交点,需要计算出射线的向量要缩放多少才能刚好与平面相接触,这就是缩放因子(scaling factor),然后用这个被缩放的向量平移射线的点来找出这个相交点

缩放因子

一个平面的法向向量有无数个,比如 n1向量: (0,1,0) 、 n2向量:(0,2,0) 都是x-z平面的法向量,和平面内两个不共线的向量都垂直的向量就是法向量,例如 a向量: (1,0,0) 和 b向量:(0,0,1) ,可以发现 a 和 n1 点乘为 0,b 和 n1点乘为 0,因此 n1是 x-z 平面的法向向量,同理 n2 也是

而向量由两个点决定,例如,AB向量 = B点 - A点

前面的 n1向量可以有很多种得到的方式,例如:

A:(0,0,0) B:(0,1,0)

A:(1,0,1) B:(1,1,1)

......

从上面得到 n1向量 的方式看,n1 可以在x-z平面进行平移,区别只是得到 n1向量的方式不同


向量.jpg

从上图可以看出,要计算缩放因子,首先创建一个向量,它在射线的起点和平面上的一个点之间,即上图中的向量 b ,缩放因子也就是:|b|/|a|

而向量a 和向量b 与平面的法向向量之间的点乘(dot product)可以计算得到,也就是说,可以通过 (a•n) / (b•n) 的方式得到缩放因子

点乘计算方式(可以用于计算两个向量之间的夹角):xa * xb + ya * yb + za * zb

几何意义(b向量在a向量方向上的投影):向量a • 向量b = |a|•|b|• cos(ab)

当射线与平面平行时,一个特殊的情况就会发生:在这种情况下,射线与平面是不可能有交点的。射线会与平面的法向向量相垂直,它们之间的点积将为0,当我们想计算缩放因子的时候,会得到一个除以0的除法

得到射线和平面的相交点
public class Geometry {
    private static final String TAG = "Geometry";

    ......
    /**
     * 计算射线和平面的相交点
     *
     * @param ray
     * @param plane
     * @return
     */
    public static Point intersectionPoint(Ray ray, Plane plane) {
        Vector rayToPlaneVector = Vector.vectorBetween(ray.getPoint(), plane.getPoint());
        Log.d(TAG, "intersectionPoint: rayToPlaneVector=" + rayToPlaneVector);
        float rayToPlaneDotProduct = rayToPlaneVector.dotProduct(plane.getVector());
        float dotProduct = ray.getVector().dotProduct(plane.getVector());
        Log.d(TAG, "intersectionPoint: rayToPlaneDotProduct=" + rayToPlaneDotProduct + " =" + dotProduct);
        float scaleFactor = rayToPlaneDotProduct / dotProduct;
        Log.d(TAG, "intersectionPoint: scaleFactor=" + scaleFactor);
        Point intersectionPoint = ray.getPoint().translate(ray.getVector().scale(scaleFactor));
        return intersectionPoint;
    }
}

移动物体

移动物体并让物体保持在边界内

// 木槌的前后左右边界值
private static final float LEFT_BOUNDS = -0.5f;
private static final float RIGHT_BOUNDS = 0.5f;
private static final float FAR_BOUNDS = -0.8f;
private static final float NEAR_BOUNDS = 0.8f;
......
    
    /**
     * 处理拖拽事件
     *
     * @param normalizedX
     * @param normalizedY
     */
    @Override
    public void handleDrag(float normalizedX, float normalizedY) {
        if (mIsPressed) {
            // 把被触碰的点投射到一条射线上
            Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);
            Log.d(TAG, "handleDrag: ray" + ray);
            Plane plane = new Plane(new Point(0f, 0f, 0f), new Vector(0f, 1f, 0f));
            // 射线与桌子平面的交点
            Point touchPoint = Geometry.intersectionPoint(ray, plane);
            Log.d(TAG, "handleDrag: touchPoint=" + touchPoint);

            // 约束物体位置,使其保持在边界内
            malletPosition = new Point(
                    clamp(touchPoint.getX(), LEFT_BOUNDS + mMallet.getRadius(), RIGHT_BOUNDS - mMallet.getRadius()),
                    touchPoint.getY(),
                    clamp(touchPoint.getZ(), 0 + mMallet.getRadius(), NEAR_BOUNDS - mMallet.getRadius())
            );

            // 碰到了冰球
            float distance = Vector.vectorBetween(malletPosition, mPuckPosition).length();
            // 如果碰到了冰球,计算其移动的方向
            if (distance < (mMallet.getRadius() + mPuck.getRadius())) {
                mPuckVector = Vector.vectorBetween(previousMalletPosition, malletPosition);
            }
            previousMalletPosition = malletPosition;
        }
    }

    /**
     * 将value值控制在min和max之间
     *
     * @param value
     * @param min
     * @param max
     * @return
     */
    private float clamp(float value, float min, float max) {
        return Math.min(max, Math.max(value, min));
    }

给冰球增加速度、方向和边界反射

@Override
public void onDrawFrame(GL10 gl10) {
    glClear(GL_COLOR_BUFFER_BIT);
    positionTableOnScreen();
    mTextureProgram.useProgram();
    mTextureProgram.setUniforms(modelViewProjectionMatrix, mTextureId);
    mTable.bindData(mTextureProgram);
    mTable.draw();

    // 使用拖拽计算得到的木槌位置
    positionObjectOnScreen(malletPosition.getX(), malletPosition.getY(), malletPosition.getZ());
    mColorProgram.useProgram();
    mColorProgram.setUniforms(modelViewProjectionMatrix, 0f, 1f, 0f);
    mMallet.bindData(mColorProgram);
    mMallet.draw();

    // 控制冰球的范围和速度
    if (mPuckPosition.getX() <= LEFT_BOUNDS + mPuck.getRadius() || mPuckPosition.getX() >= RIGHT_BOUNDS - mPuck.getRadius()) {
        mPuckVector = new Vector(-mPuckVector.getX(), mPuckVector.getY(), mPuckVector.getZ());
        mPuckVector = mPuckVector.scale(0.8f);
    }
    if (mPuckPosition.getZ() <= FAR_BOUNDS + mPuck.getRadius() || mPuckPosition.getZ() >= NEAR_BOUNDS - mPuck.getRadius()) {
        mPuckVector = new Vector(mPuckVector.getX(), mPuckVector.getY(), -mPuckVector.getZ());
        mPuckVector = mPuckVector.scale(0.8f);
    }
    // 将冰球向它的速度方向移动一点距离
    mPuckPosition = mPuckPosition.translate(mPuckVector);
    // 使用拖拽计算得到的冰球位置
    positionObjectOnScreen(mPuckPosition.getX(), mPuckPosition.getY(), mPuckPosition.getZ());
    mColorProgram.useProgram();
    mColorProgram.setUniforms(modelViewProjectionMatrix, 1f, 0f, 0f);
    mPuck.bindData(mColorProgram);
    mPuck.draw();

    // 渐渐缩小冰球移动的速度
    mPuckVector = mPuckVector.scale(0.99f);
}

效果

触摸反馈.gif
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容