OpenGl-ES2.0 For Android 读书笔记(四)

一、开始

桌面我们已经绘制好了,有没有觉得这两个点看着特别不舒服,所以现在我们就来优化下这两个点,最终我们要做出的效果是这个样子的:

AirHockey.gif

并且我们还要给这两个点加上触摸事件,让我们的游戏可以完成了。

二、创建简单对象

我们将会学习到两个知识点:
1.什么是Triangle Strip?
2.如何使用view矩阵。
我们先学习第一点,之前我们学过Triangle FanTriangle Strip跟它类似,看下图:

Triangle Strip.png

它是按照顺序绘制多个三角形的,我们可以看到我们的冰球棒大概是这个样子的:

冰球棒.png

我们要怎么去画这个冰球棒呢?我们发现其实他是由两个圆柱体组成的,所以我们只需要实现一个画圆柱体的方法,再把两个圆柱体拼起来就好了。那要怎么去画一个圆柱体呢?我们可以再把这个圆柱体分离一下,它其实是由上面的一个圆,和下面一个由矩形卷起来的东西组成的,所以我们可以把它再拆分成画一个圆和画一个矩形。
首先我们来画圆,我们知道OpenGL只能画点,线,三角形,如何去画圆呢?我们可以用Trangle Fan去实现,当画的三角形多了,可以造成画圆的假象。
我们新建objects包,并创建一个ObjectBuilder.java类去实现如下功能:

• The caller can decide how many points the object should have. The more points, the smoother the puck or mallet will look.
• The object will be contained in one floating-point array. After the object is built, the caller will have one array to bind to OpenGL and one command to draw the object.
• The object will be centered at the caller’s specified position and will lie flat on the x-z plane. In other words, the top of the object will point straight up.

这是原文的说明:就是说我们构建的时候需要传入一个参数,构建对象的点的个数,当点的个数越多的时候,我们构建的对象将会看起来更好些,然后我们构建对象的数据必须在一个数组中,然后最后一个的意思是我们的冰球棒应该是垂直于X-Z平面的。接下来就让我们去实现代码吧,首先我们要在util包下新建一个类Geometry.java实现如下代码:

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

}

Geometry.java主要是对形状的一些定义,然后我们再去实现ObjectBuilder.java里面的代码:

public class ObjectBuilder {

    private static final int FLOATS_PER_VERTEX = 3;
    private float[] mVertexData;
    private int mOffset = 0;

    private ArrayList<DrawCommand> mDrawList = new ArrayList<>();

    private ObjectBuilder(int vertexNums){
        mVertexData = new float[vertexNums * FLOATS_PER_VERTEX];
    }

    private static int sizeOfCircleInVertices(int numPoints) {
        return 1 + (numPoints + 1);
    }

    private static int sizeOfOpenCylinderInVertices(int numPoints) {
        return (numPoints + 1) * 2;
    }

    public static GeneratedData createPuck(Geometry.Cylinder puck,int numPoints){
        int size = sizeOfCircleInVertices(numPoints)
                + sizeOfOpenCylinderInVertices(numPoints);

        ObjectBuilder objectBuilder = new ObjectBuilder(size);

        Geometry.Circle puckTop =
                new Geometry.Circle(puck.center.translateY(puck.height / 2f), puck.radius);

        objectBuilder.appendCircle(puckTop , numPoints);
        objectBuilder.appendOpenCylinder(puck , numPoints);

        return objectBuilder.build();
    }

    public static GeneratedData createMallet(Geometry.Point center ,
                                      float radius , float height , int numPoints){

        int size = (sizeOfCircleInVertices(numPoints) + sizeOfOpenCylinderInVertices(numPoints))*2;

        ObjectBuilder objectBuilder = new ObjectBuilder(size);

        // 先创建底部的圆柱体
        float baseHeight = height * 0.25f;
        Geometry.Circle baseCircle =
                new Geometry.Circle( center.translateY(-baseHeight), radius);
        Geometry.Cylinder baseCylinder =
                new Geometry.Cylinder( baseCircle.center.translateY(-baseHeight / 2f), radius, baseHeight);
        objectBuilder.appendCircle(baseCircle, numPoints);
        objectBuilder.appendOpenCylinder(baseCylinder, numPoints);

        //再创建把手的圆柱体
        float handleHeight = height * 0.75f; float handleRadius = radius / 3f;
        Geometry.Circle handleCircle =
                new Geometry.Circle( center.translateY(height * 0.5f), handleRadius);
        Geometry.Cylinder handleCylinder =
                new Geometry.Cylinder( handleCircle.center.translateY(-handleHeight / 2f), handleRadius, handleHeight);
        objectBuilder.appendCircle(handleCircle, numPoints);
        objectBuilder.appendOpenCylinder(handleCylinder, numPoints);

        return objectBuilder.build();
    }

    private void appendCircle(Geometry.Circle circle , int numPoints){
        final int startVertex = mOffset / FLOATS_PER_VERTEX;
        final int numVertices = sizeOfCircleInVertices(numPoints);

        mVertexData[mOffset++] = circle.center.x;
        mVertexData[mOffset++] = circle.center.y;
        mVertexData[mOffset++] = circle.center.z;

        for (int i = 0; i <= numPoints ; i++) {
            float angleInRadians = ((float) i / (float) numPoints) * ((float) Math.PI * 2f);

            mVertexData[mOffset++] = circle.center.x
                    + (float) (circle.radius * Math.cos(angleInRadians));
            mVertexData[mOffset++] = circle.center.y;
            mVertexData[mOffset++] = circle.center.z
                    + (float) (circle.radius * Math.sin(angleInRadians));
        }

        mDrawList.add(new DrawCommand() {
            @Override
            public void draw() {
                glDrawArrays(GL_TRIANGLE_FAN , startVertex , numVertices);
            }
        });

    }

    private void appendOpenCylinder(Geometry.Cylinder cylinder , int numPoints){
        final int startVertex = mOffset / FLOATS_PER_VERTEX;
        final int numVertices = sizeOfOpenCylinderInVertices(numPoints);

        final float yStart = cylinder.center.y - cylinder.height/2f;
        final float yEnd = cylinder.center.y + cylinder.height/2f;

        for (int i = 0; i <= numPoints ; i++) {
            float angleInRadians = ((float)i / (float) numPoints) * ((float) Math.PI *2f);

            mVertexData[mOffset++] = cylinder.center.x
                    + (float) (cylinder.radius * Math.cos(angleInRadians));
            mVertexData[mOffset++] = yStart;
            mVertexData[mOffset++] = cylinder.center.z
                    + (float)(cylinder.radius*Math.sin(angleInRadians));

            mVertexData[mOffset++] = cylinder.center.x
                    + (float) (cylinder.radius * Math.cos(angleInRadians));
            mVertexData[mOffset++] = yEnd;
            mVertexData[mOffset++] = cylinder.center.z
                    + (float)(cylinder.radius*Math.sin(angleInRadians));

        }

        mDrawList.add(new DrawCommand() {
            @Override
            public void draw() {
                glDrawArrays(GL_TRIANGLE_STRIP , startVertex , numVertices);
            }
        });

    }

    public static interface DrawCommand {
        void draw();
    }

    public static class GeneratedData {

        public final float[] vertexData;
        public final List<DrawCommand> drawList;

        GeneratedData(float[] vertexData, List<DrawCommand> drawList) {
            this.vertexData = vertexData;
            this.drawList = drawList;
        }
    }

    private GeneratedData build() {
        return new GeneratedData(mVertexData, mDrawList);
    }

}

我们先看createPuck()方法,先计算了我们需要绘制的点的个数,然后我们用一个圆和一个卷起来的矩形组成了我们需要的圆柱体,我们的思路就是这样的,对照代码应该就能看懂了,现在我们已经把我们的冰球棒这个物体造好了,现在就需要知道如何在哪里去绘制它了。
首先我们需要了解下一些坐标系的转换:

坐标系转换.png

现在我们创建的物体的坐标是Model Coordinates乘上Model矩阵之后就是这个物体在World Coordinates里面的坐标了,然后再乘上View矩阵就是我们在相机位置看到的坐标了,也就是在Camera Coordinates的坐标了,最后再乘上Projection矩阵就是我们的物体在屏幕上显示的位置,这个东西在书中感觉说的不太清楚,大家可以看这个地方的说明:http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-3-matrices/
Model矩阵主要就是设置Model放置的位置,我们可以用Model矩阵去做一些移动,旋转的操作。

首先我们在data包下创建Puck.java类,实现下面的代码:

public class Puck {

    private static final int POSITION_COMPONENT_COUNT = 3;

    public final float mRadius , mHeight;

    private final VertexArray mVertexArray;

    private final List<ObjectBuilder.DrawCommand> mDrawList;

    public Puck(float radius , float height , int numPoints){
        mRadius = radius;
        mHeight = height;

        ObjectBuilder.GeneratedData data = ObjectBuilder.createPuck(
                new Geometry.Cylinder(new Geometry.Point(0,0,0) , radius , height), numPoints);

        mVertexArray = new VertexArray(data.vertexData);
        mDrawList = data.drawList;
    }

    public void bindData(ColorShaderProgram program){
        mVertexArray.setVertexAttribPointer(0 ,
                program.getPositionAttributeLocation() , POSITION_COMPONENT_COUNT , 0);
    }

    public void draw(){
        for (ObjectBuilder.DrawCommand command :
                mDrawList) {
            command.draw();
        }
    }

}

同时我们修改Mallet.java如下:

public class Mallet {

    private static final int POSITION_COMPONENT_COUNT = 3;
//    private static final int COLOR_COMPONENT_COUNT = 3;
//    private static final int STRIDE = (POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT)
//            * Constants.BYTE_PRE_FLOAT;

//    private static final float[] VERTEXT_DATA = new float[]{
//            // Order of coordinates: X, Y, R, G, B
//            0f, -0.4f, 0f, 0f, 1f,
//            0f, 0.4f, 1f, 0f, 0f
//    };

    private VertexArray mVertexData;
    private List<ObjectBuilder.DrawCommand> mDrawList;

    public final float mRadius,mHeight;

    public Mallet(float radius , float height , int numPoints){
        mRadius = radius;
        mHeight = height;

        ObjectBuilder.GeneratedData data = ObjectBuilder.createMallet(
                new Geometry.Point(0 , 0 , 0 ) , radius , height , numPoints);

        mVertexData = new VertexArray(data.vertexData);
        mDrawList = data.drawList;
    }

    public void bindData(ColorShaderProgram program){
        mVertexData.setVertexAttribPointer(0 ,
                program.getPositionAttributeLocation() ,
                POSITION_COMPONENT_COUNT , 0);
    }

    public void draw(){
        for (ObjectBuilder.DrawCommand command :
                mDrawList) {
            command.draw();
        }
    }

}

大家可能发现了,我们bindData()的时候没有设置颜色了,所以我们还需要修改下我们的Shader程序。我们先移除ShaderProgram.javaColorShaderProgram.java中所有跟a_Color相关的代码,然后在ShaderProgram.java中加上如下声明:

protected static final String U_COLOR = "u_Color";

ColorShaderProgram.java中加上如下代码:

private final int mUColorLocation;

并在构造方法中获取该值:

mUColorLocation = glGetUniformLocation(mProgram , U_COLOR);

最后修改setUniforms()方法如下:

public void setUniforms(float[] matrix , float r , float g , float b) {
        // 把矩阵传递给渲染程序
        glUniformMatrix4fv(mUMatrixLocation, 1, false, matrix, 0);
        glUniform4f(mUColorLocation , r , g , b , 1f);
}

当然我们还要修改.glsl代码,修改simple_vertex_shader.glsl如下:

uniform mat4 u_Matrix;

attribute vec4 a_Position;

void main() {
    gl_Position = u_Matrix * a_Position;
    gl_PointSize = 10.0;
}

修改simple_fragment_shader.glsl如下:

precision mediump float;

uniform vec4 u_Color;

void main() {
    gl_FragColor = u_Color;
}

现在我们就可以去处理绘制过程了,首先我们在TextureRenderer.java中声明冰球的对象:

private Puck mPuck;

然后我们修改冰球棒和冰球的赋值语句如下:

mMallet = new Mallet(0.08f, 0.15f, 32);
mPuck = new Puck(0.06f, 0.02f, 32);

我们接下来设置一下我们的camera的位置,先声明下View矩阵,和其他要用到的矩阵:

private float[] mViewMatrix = new float[16];
private float[] mProjectionViewMatrix = new float[16];
private float[] mProjectionViewModelMatrix = new float[16];

然后修改onSurfaceChanged()方法中的代码如下:

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

        MatrixHelper.perspectiveM(mProjectionMatrix , 45 , (float)width / (float)height , 1f , 10f);
        setLookAtM(mViewMatrix, 0, 0f, 1.2f, 2.2f, 0f, 0f, 0f, 0f, 1f, 0f);
//        setIdentityM(mModelMatrix, 0);
//        translateM(mModelMatrix, 0, 0f, 0f, -2.5f);
//        rotateM(mModelMatrix, 0 , -60 , 1 , 0 , 0);
//
//        final float[] temp = new float[16];
//        multiplyMM(temp, 0, mProjectionMatrix, 0, mModelMatrix, 0);
//        System.arraycopy(temp, 0, mProjectionMatrix, 0, temp.length);
 }

这里解释下setLookAtM()方法:

setLookAtM.png

意思就是我们现在的视角是从(0,1.2,2.2)的位置将会看向(0,0,0)的位置,然后你的头是朝向上面的。
最后就是修改绘制了,修改如下:

@Override
    public void onDrawFrame(GL10 gl10) {
        glClear(GL_COLOR_BUFFER_BIT);

        multiplyMM(mProjectionViewMatrix , 0 ,mProjectionMatrix , 0 , mViewMatrix , 0);

        positionTableInScene();
        mTextureShaderProgram.useProgram();
        mTextureShaderProgram.setUniforms(mProjectionViewModelMatrix , mTexture);
        mTable.bindData(mTextureShaderProgram);
        mTable.draw();

        positionObjectInScence(0 , mMallet.mHeight / 2 , -0.4f);
        mColorShaderProgram.useProgram();
        mColorShaderProgram.setUniforms(mProjectionViewModelMatrix , 1 , 0 , 0);
        mMallet.bindData(mColorShaderProgram);
        mMallet.draw();

        positionObjectInScence(0 , mMallet.mHeight / 2 , 0.4f);
        mColorShaderProgram.setUniforms(mProjectionViewModelMatrix , 0 , 0 , 1);
        mMallet.draw();

        positionObjectInScence(0f, mPuck.mHeight / 2f, 0f);
        mColorShaderProgram.setUniforms(mProjectionViewModelMatrix, 0.8f, 0.8f, 1f);
        mPuck.bindData(mColorShaderProgram);
        mPuck.draw();

//        mTextureShaderProgram.useProgram();
//        mTextureShaderProgram.setUniforms(mProjectionMatrix , mTexture);
//        mTable.bindData(mTextureShaderProgram);
//        mTable.draw();
//
//        mColorShaderProgram.useProgram();
//        mColorShaderProgram.setUniforms(mProjectionMatrix);
//        mMallet.bindData(mColorShaderProgram);
//        mMallet.draw();
}

positionTableInScene()方法如下:

private void positionTableInScene(){
        setIdentityM(mModelMatrix , 0);
        rotateM(mModelMatrix , 0 , -90 , 1 , 0 , 0);
        multiplyMM(mProjectionViewModelMatrix , 0 , mProjectionViewMatrix , 0 , mModelMatrix , 0);
    }

positionObjectInScence()方法如下:

private void positionObjectInScence(float x , float y , float z){
        setIdentityM(mModelMatrix , 0);
        translateM(mModelMatrix , 0 , x , y , z);
        multiplyMM(mProjectionViewModelMatrix , 0 , mProjectionViewMatrix , 0 , mModelMatrix , 0);
    }

因为之前Table是在XY平面上画的,所以现在要把模型旋转90度。现在运行下看看吧,效果就出来了。

三、实现触摸效果

我们现在已经把桌面,冰球,冰球棒准备好了,如果想玩这个游戏的话,现在就差跟用户的交互了,好的,那我们就开始来做交互吧。
我们的思路是这样的,我们现在可以拿到我们手机触摸到屏幕上的点的坐标,所以我们需要把屏幕上的点的坐标转换成在World Coornidates里面的一条射线,然后看这条射线是否跟我们的冰球棒有相交,如果有相交的话,就说明我们碰到了冰球棒,现在就让我们用代码来实现吧!
首先在设置GLView之前设置GLView的onTouch事件如下:

mGLSurfaceView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if (motionEvent != null){
                    float normalX = (motionEvent.getX()/ (float)view.getWidth()) * 2 - 1;
                    float normalY = -((motionEvent.getY()/ (float)view.getHeight()) * 2 - 1);
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN){

                    }else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE){

                    }
                }
                return false;
            }
        });

我们现在把屏幕上的坐标转换成了正常的设备坐标(normalized device coordinates),接下来就是把这个坐标转换成world coordinates里面的射线了,我们现在Geometry.java里面做一些定义:

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 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 static Vector vectorBetween(Point from, Point to) {
        return new Vector(
                to.x - from.x,
                to.y - from.y,
                to.z - from.z);
    }

    public static class Sphere { 
        public final Point center; 
        public final float radius;
        
        public Sphere(Point center, float radius) { 
            this.center = center;
            this.radius = radius;
        }
    }

为了简单一些,我们把冰球棒当成一个球体去处理,然后我们现在就开始想办法去把我们的坐标转换成射线了:

private Geometry.Ray convert2DPointToRay(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 , mInvertedViewProjectionMatrix , 0 , nearPointNdc , 0);
        multiplyMV(farPointWorld , 0 , mInvertedViewProjectionMatrix , 0 , farPointNdc , 0);

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

其中有个全局变量mInvertedViewProjectionMatrix是逆转矩阵,需要在onDrawFrame()

multiplyMM(mProjectionViewMatrix , 0 ,mProjectionMatrix , 0 , mViewMatrix , 0);

后面调用

//生成逆转矩阵
invertM(mInvertedViewProjectionMatrix , 0 , mProjectionViewMatrix , 0);

生成,这个矩阵可以把normalized device coordinates的坐标逆转成world coordinates里面的坐标,当然也生成了逆转的w值。然后我们就生成了平台锥体近端和远端的两个点,然后用两个点生成了一条射线,现在再判断这条射线是否穿过了冰球棒的球体就知道我们是否碰到了冰球棒了。那如何判断射线是否穿过了球体呢?这个简单,判断球体球心到射线的距离是不是小于半径不就知道了,接下来就用代码实现呗!
我们在Geometry.java里面实现这个比较的方法,因为改动了之前的一些定义,直接贴上全部的代码,大家自己去比对吧!

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

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

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

    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 class Sphere {
        public final Point center;
        public final float radius;

        public Sphere(Point center, float radius) {
            this.center = center;
            this.radius = radius;
        }
    }

    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 void handleTouchPress(float normalizedX , float normalizedY){
        Geometry.Ray ray = convert2DPointToRay(normalizedX , normalizedY);
        Geometry.Sphere sphere = new Geometry.Sphere(mBlueMalletPosition , mMallet.mRadius);
        mMalletPressed = Geometry.intersects(sphere , ray);
    }

mMalletPressed是表示冰球棒是否有被碰到的变量,mBlueMalletPosition是表示蓝色的冰球棒的位置的变量,需要在onSurfaceCreated()中初始化:

mBlueMalletPosition = new Geometry.Point(0 , mMallet.mHeight/2 , 0.4f);

接下来我们要做的就是移动我们的冰球棒了,好了,现在我们的思路就是,我们要找到我们触碰的屏幕上的点在World Coordinates里射线跟我们桌面的交点,然后把我们的冰球棒移动到那个位置。
首先我们找交点,我们先在Geometry.java中做一些定义:

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

dotProduct ()scale()是Vector的方法:

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 void handleTouchDrag(float normalizedX , float normalizedY){
        if (mMalletPressed){
            Geometry.Ray ray = convert2DPointToRay(normalizedX, normalizedY);

            Geometry.Plane plane = new Geometry.Plane(new Geometry.Point(0, 0, 0),
                    new Geometry.Vector(0, 1, 0));

            Geometry.Point touchPoint = Geometry.intersectionPoint(ray , plane);

            mBlueMalletPosition = new Geometry.Point(touchPoint.x , mMallet.mHeight/2 , touchPoint.z);
        }
    }

然后我们修改onDrawFrame()方法中的蓝色冰球棒positionObjectInScence()方法:

positionObjectInScence(mBlueMalletPosition.x , mBlueMalletPosition.y , mBlueMalletPosition.z);

最后在onTouch()方法里面调用press,drag的方法:

mGLSurfaceView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if (motionEvent != null){
                    final float normalX = (motionEvent.getX()/ (float)view.getWidth()) * 2 - 1;
                    final float normalY = -((motionEvent.getY()/ (float)view.getHeight()) * 2 - 1);
                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN){
                        mGLSurfaceView.queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                renderer.handleTouchPress(normalX , normalY);
                            }
                        });
                    }else if (motionEvent.getAction() == MotionEvent.ACTION_MOVE){
                        mGLSurfaceView.queueEvent(new Runnable() {
                            @Override
                            public void run() {
                                renderer.handleTouchDrag(normalX , normalY);
                            }
                        });
                    }
                    return true;
                }
                return false;
            }
        });

运行看看,我们是不是可以拖动我们的蓝色的冰球棒了,但是你可能也发现了,我们的冰球棒居然可以拖出桌面,这个我们需要处理下了,我们给冰球棒定义一个可以活动的范围:

private final float mLeftBound = -0.5f;
private final float mRightBound = 0.5f;
private final float mFarBound = -0.8f;
private final float mNearBound = 0.8f;

还需要定义一个方法:

private float clamp(float value, float min, float max) {
        return Math.min(max, Math.max(value, min));
    }

然后在设置蓝色冰球棒绘制的位置的地方需要做些改动:

mBlueMalletPosition = new Geometry.Point(
                    clamp(touchPoint.x , mLeftBound + mMallet.mRadius , mRightBound - mMallet.mRadius),
                    mMallet.mHeight/2 ,
                    clamp(touchPoint.z , 0 + mMallet.mRadius , mNearBound - mMallet.mRadius));

再运行看看,是不是出不了桌面了。最后我们还需要让冰球棒和冰球之间有些互动,不然,关移动冰球棒也没什么意思。要让他们之间有互动,我们需要解决两个问题,那就是是冰球要往哪个方向移动,要以什么速度移动?现在我们就来用代码实现。
我们先做如下声明:

private Geometry.Point mPreBlueMalletPosition;
private Geometry.Point mPuckPosition;
private Geometry.Vector mPuckVector;

mPreBlueMalletPosition用来储存蓝色冰球棒原来的位置,mPuckPosition用来储存冰球的位置,mPuckVector用来记录冰球移动的方向和速度。
handleTouchDrag()适当的地方给mPreBlueMalletPosition赋值,然后在onSurfaceCreated()里初始化mPuckPositionmPuckVector:

mPuckPosition = new Geometry.Point(0 , mPuck.mHeight / 2 , 0);
mPuckVector = new Geometry.Vector(0 , 0 , 0);

然后再handleTouchDrag()方法里面判断蓝色冰球棒是否有跟冰球碰撞,如果有就记录下冰球要移动的方向和速度:

float distance = Geometry.vectorBetween(mBlueMalletPosition , mPuckPosition).length();
if (distance < (mMallet.mRadius + mPuck.mRadius)){
    mPuckVector = Geometry.vectorBetween(mPreBlueMalletPosition , mBlueMalletPosition);
}

然后在onDrawFrame()中实现如下代码让冰球移动:

mPuckPosition = mPuckPosition.translate(mPuckVector);
positionObjectInScence(mPuckPosition.x, mPuckPosition.y, mPuckPosition.z);

但是这样会发生我们之前没有给冰球棒设置边界的时候一样的问题,冰球也会移动到桌面外面去,所以我们也要给冰球加上边界:

if (mPuckPosition.x < mLeftBound + mPuck.mRadius
        || mPuckPosition.x > mRightBound - mPuck.mRadius) {
        mPuckVector = new Geometry.Vector(-mPuckVector.x, mPuckVector.y, mPuckVector.z);
}
if (mPuckPosition.z < mFarBound + mPuck.mRadius
        || mPuckPosition.z > mNearBound - mPuck.mRadius) {
        mPuckVector = new Geometry.Vector(mPuckVector.x, mPuckVector.y, -mPuckVector.z);
}
mPuckPosition = new Geometry.Point(
        clamp(mPuckPosition.x, mLeftBound + mPuck.mRadius, mRightBound - mPuck.mRadius),
        mPuckPosition.y,
        clamp(mPuckPosition.z, mFarBound + mPuck.mRadius, mNearBound - mPuck.mRadius)
        );

现在运行下看看吧!但是我们会发现冰球的速度是不会变化的,有没有觉得这样不太好,速度应该是会衰减的,所以我们加上如下代码,效果就会好一些了:

mPuckVector = mPuckVector.scale(0.99f);

现在就完美了,我们的冰球游戏算是完成了!
这篇文章终于写完了,从晚上12点写到现在3点多了。。。。睡觉了睡觉了。。。。

项目代码在这里:https://github.com/KevinKmoo/AirHockeyWithBetterMallet

能力有限,自己读书的学习所得,有错误请指导,轻虐!
转载请注明出处。----by kmoo

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

推荐阅读更多精彩内容