OpenGL.ES在Android上的简单实践:10-曲棍球(拖动物体、碰撞测试)

1、让木槌跟随手指移动

继续上一篇文章9的内容。既然可以测试木槌是否被触碰了,我们将继续努力下去:当我们来回拖动木槌的时候,它要去哪里?我们可以用这种方式思考:木槌平放在桌面上,当我们来回移动手指的时候,木槌应该随着手指移动并继续平放在桌子上。我们可以通过执行 射线-平面(Ray-Plane) 相交测试计算出它的正确位置。

以上的理论分析,我们又多出了一个几何概念——平面。在我们的认知中,两点相连成一直线,两线相交成一平面。所以,我们是否为平面(Plane)定义两个相交的向量?此时我们引入另外一个新的数学概念 法向量。简单介绍一下法向量:法向量是空间解析几何的一个概念,垂直于平面的直线所表示的向量为该平面的法向量。由于空间内有无数个直线垂直于已知平面,因此一个平面都存在无数个法向量(包括两个单位法向量)。法向量也成为法线。而且不单止光滑平面才有法向量,曲面在某点P处的法线为垂直于该点切平面的向量。也就是说,一个光滑的球,它的表面也是有无限个法向量。

有了以上知识,我们在Geometry.java更新代码,我们定义的平面就非常简单了:它包含一个法向向量(normal vector)和平面上的一个点:法向向量仅仅是一个垂直于那个平面的一个向量。平面也有其可能的定义(两个相交的向量),但这个是简单的,易于使用的定义。

public static class Plane {public final Point point;public final Vector normal;public Plane(Point point,Vector normal) {this.point = point;this.normal = normal;}
}

下面是一个平面与射线相交的栗子。一个平面位于(0,0,0),法向量(0,1,0);这里还有一条射线,位于(-2,1,0),且向量为(1,-1,0)。我们要使用这个平面和射线解释相交测试,并分析如何计算出这个交点。

接下来我们开始搬砖写代码了。首先我们从handleTouchMove开始:

private void handleTouchMove(float normalizedX, float normalizedY) {if(malletPressed) { // 根据屏幕触碰点 和 视图投影矩阵 产生三维射线Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);// 定义的桌子平面,观察平面的点为(0,0,0)Geometry.Plane tablePlane = new Geometry.Plane(new Geometry.Point(0,0,0), new Geometry.Vector(0,1,0));// 进行射线-平面 相交测试Geometry.Point touchedPoint = Geometry.intersectionPoint(ray, tablePlane);// 根据相交点 更新木槌位置malletPosition = new Geometry.Point(touchedPoint.x, touchedPoint.y, touchedPoint.z);}
}

只有当我们开始使用手指按住那个木槌时,我们才想要拖动它,如果是,那我们就做射线转换,它与我们上一篇文章9中的转换理论一样。一旦我们有了表示被触碰点的射线,我们就要找出这条射线与表示曲棍球桌子的平面在哪里相交了,随即就能更新木槌的位置。

要想计算这个交点,首先需要把射线方向向量放大(缩小)到一定情况,才能和平面想接触;这个放大缩小倍数就是缩放因子(scale factor)接下来我们用这个被缩放的方向向量 平移射线的起点来找出这个相交的点。

要计算这个缩放因子(scale factor)我们首先创建一个向量,它在射线的起点和平面的一个点之间。然后计算那个向量与平面法向向量之间的点积(dot product)(上文我们提及过叉积,叉积与点积的区别,请参考这里的向量积与数量积的区别)

之后我们计算这个缩放量:我们用  射线起点到平面的向量 与 平面法向向量的点积 除以 射线方向向量 与 平面法向向量的点积。这就得到了我们需要的缩放因子。

其中更详尽的数学原理,请参考wikipedia,要想更深入的探索,可以到YouTube或B站搜索3blue1brown,能更好的探究线性代数的本质与几何意义。有了以上理论分析,我们开始编写 Geometry.intersectionPoint :

package org.zzrblog.blogapp.utils.Geometry.java;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),(x*other.z) - (z*other.x),(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 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;} // 产生 射线起点 到 平面视点的向量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;}

单纯的代码可能不能容易理解,我们继续使用上面的栗子:一个平面位于(0,0,0),法向量(0,1,0);这里还有一条射线,位于(-2,1,0),且向量为(1,-1,0)。如果我们把这个向量扩展得足够远,这个射线会在哪里碰到平面呢?让我们运用以上数学公式找出答案吧。

首先,我们需要把平面与射线之间的向量赋值给rayToPlaneVector。它应该被设为(0,0,0)-(-2,1,0)=(2,-1,0)

然后,下一步是计算scaleFactor。我们先计算 rayToPlaneVector与normal的点积 = (2,-1,0).(0,1,0)= -1;接着就是计算射线方向向量与normal的点积 = (1,-1,0).(0,1,0)= -1;所以缩放因子为 -1 / -1 = 1;

得到缩放因子后,我们先把方向向量进行缩放=(1,-1,0)* 1 = (1,-1,0);然后把射线点按缩放后的方向向量进行平移,即:(-2,1,0)+(1,-1,0)=(-1,0,0)。这就是射线与平面相交的位置。

既然相交点已经找出来了,我们就可以直接利用相交点更新木槌的位置变量,但有一点要注意,我们要保持y轴的值落在桌子固定高度,进而更新木槌的模型矩阵:

    public void handleTouchMove(float normalizedX, float normalizedY) {if(malletPressed) {// 根据屏幕触碰点 和 视图投影矩阵 产生三维射线Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);// 定义的桌子平面,观察平面的点为(0,0,0)Geometry.Plane tablePlane = new Geometry.Plane(new Geometry.Point(0,0,0), new Geometry.Vector(0,1,0));// 进行射线-平面 相交测试Geometry.Point touchedPoint = Geometry.intersectionPoint(ray, tablePlane);// 根据相交点 更新木槌位置malletPosition = new Geometry.Point(touchedPoint.x, mallet.height, touchedPoint.z);// 更新mallet.modelMatrixMatrix.setIdentityM(mallet.modelMatrix, 0);Matrix.translateM(mallet.modelMatrix,0, malletPosition.x, malletPosition.y, malletPosition.z);}}

现在,加上调试日志,把应用程序跑起来,看看实际效果?

2、边界碰撞检测

我们如愿以偿能来回拖动木槌了,但你可能已经注意到问题了:木槌可以走到桌子边界外面去,如下图所示。我们要添加一些基本的碰撞检测,让木槌待在它应该在的地方。我们也会运用一些基本的物理原理,让我们的木槌可以在桌子上击打曲棍球。

边缘的检测比较简单,我们打开objects下的Table.java查看桌子的顶点数据定义。

private static final float[] VERTEX_DATA = {//x,    y,      s,      t0f,     0f,     0.5f,   0.5f,-0.5f,  -0.8f,  0f,     0.9f,0.5f,   -0.8f,  1f,     0.9f,0.5f,   0.8f,   1f,     0.1f,-0.5f,  0.8f,   0f,     0.1f,-0.5f,  -0.8f,  0f,     0.9f,};

我们定义的桌子的顶点数据是定义在x-y的平面上的,然后我们用模型矩阵把桌子平面沿x轴旋转了-90°,让桌子的平面落在x-z的平面上,所以我们可以定义四个静态变量,用来表示桌子的边缘范围的最大值

package org.zzrblog.blogapp.objects.Table.java;public static final float leftBound = -0.5f; // 左边缘
public static final float rightBound = 0.5f; // 右边缘
public static final float farBound = -0.8f; // 远平面边缘
public static final float nearBound = 0.8f; // 近平面边缘

这些定义与空气曲棍球桌子的四边相对应。现在我们可以更新handleTouchMove(),并用下面的代码代替malletPosition的赋值

     malletPosition = new Geometry.Point(clamp(touchedPoint.x, Table.leftBound+mallet.raduis, Table.rightBound-mallet.raduis),mallet.height ,clamp(touchedPoint.z, Table.farBound+mallet.raduis, Table.nearBound-mallet.raduis) );// 把触碰值 控制在 指定的最大值与最小值之间。private float clamp(float value, float max, float min) {return Math.min(max, Math.max(value, min));}

继续并再次运行这个应用,你现在应该发现木槌拒绝移动边界外了。在往下内容之前,我们把 木槌位置 和 木槌是否被按压 这两个属性封装到木槌类Mallet当中,方便管理。

    public Geometry.Point position ;public volatile boolean isPressed;public Mallet(float radius, float height, int numPointsAroundMallet){... ...}

3、木槌冰球碰撞测试

现在,我们准备着手加入最有乐趣的部分,另木槌和冰球产生激情四射的碰撞。按照惯例,我们提出必须的需求:

1、木槌碰撞冰球后,朝着哪个方向移动?

2、冰球移动的速度多快,怎么量化?

回到以上问题,我们需要随着时间变化持续跟踪木槌是如何移动的。我们要做的第一件事就是给Mallet加入一个新的变量previousPosition,用于记录前一刻的position,在handleTouchMove中加入它的赋值代码:

public void handleTouchMove(float normalizedX, float normalizedY) {if(mallet.isPressed) {// 保存前一刻木槌的位置信息mallet.previousPosition = mallet.position;... ...}
}

下一步,我们在冰球Puck类中创建 位置、移动方向等变量

    private Geometry.Vector speedVector;private Geometry.Point position;public Puck(float radius, float height, int numPointsAroundPuck) {... ...}

按照之前Mallet.position的方式,在onSurfaceChanged接口里面初始化Puck.position,如下:

    @Overridepublic void onSurfaceChanged(GL10 gl10, int width, int height) {... ...mallet.position = new Geometry.Point(0f, mallet.height/2f, 0.5f);puck.position = new Geometry.Point(0f, puck.height/2f, 0f);puck.speedVector = new Geometry.Vector(0f, 0f, 0f);}

接下来,按照逻辑分析。当木槌与冰球的距离小于两者半径之和,就会产生碰撞,并更新方向向量,我们在handleTouchMove最后添加代码,要确保这段代码是在木槌被按压后(mallet.isPressed == true)产生的:

        if(mallet.isPressed) {// 保存前一刻木槌的位置信息mallet.previousPosition = mallet.position;... ...// 检查木槌和冰球是否碰撞,更新冰球移动的方向向量float distance = Geometry.vectorBetween(mallet.position, puck.position).length();if(distance < mallet.radius + puck.radius) {puck.speedVector = Geometry.vectorBetween(mallet.previousPosition, mallet.position);puck.position = puck.position.translate(puck.speedVector);}}

当木槌击中冰球,并且我们用前一个木槌位置和当前木槌的位置给冰球创建一个方向向量。木槌移动得越快,那个向量就会越大,冰球也会移动得越快。随后利用这个方向向量,更新puck的position。(此时这里不用固定y值,因为mallet已经固定了,冰球的速度向量的y值其实每时每刻都等于0)碰撞发生致使冰球沿着速度向量移动,这个移动不需要在任何条件下,所以我们把需要时刻更新puck的position到puck的模型矩阵,实时渲染到画面上,(为了统一,我把mallet/puck的模型矩阵相关操作放到onDrawFrame接口中)最后的代码如下:

    public void handleTouchMove(float normalizedX, float normalizedY) {if(mallet.isPressed) {// 保存前一刻木槌的位置信息mallet.previousPosition = mallet.position;// 根据屏幕触碰点 和 视图投影矩阵 产生三维射线Geometry.Ray ray = convertNormalized2DPointToRay(normalizedX, normalizedY);// 定义的桌子平面,观察平面的点为(0,0,0)Geometry.Plane tablePlane = new Geometry.Plane(new Geometry.Point(0,0,0), new Geometry.Vector(0,1,0));// 进行射线-平面 相交测试Geometry.Point touchedPoint = Geometry.intersectionPoint(ray, tablePlane);// 根据相交点 更新木槌位置//malletPosition = new Geometry.Point(touchedPoint.x, mallet.height/2f, touchedPoint.z);mallet.position = new Geometry.Point(clamp(touchedPoint.x, Table.leftBound+mallet.radius, Table.rightBound-mallet.radius),mallet.height/2f, //touchedPoint.y,clamp(touchedPoint.z, Table.farBound+mallet.radius, Table.nearBound-mallet.radius));// 检查木槌和冰球是否碰撞,更新冰球移动的方向向量float distance = Geometry.vectorBetween(mallet.position, puck.position).length();if(distance < mallet.radius + puck.radius) {puck.speedVector = Geometry.vectorBetween(mallet.previousPosition, mallet.position);}}}@Overridepublic void onDrawFrame(GL10 gl10) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, table.modelMatrix,0);textureShaderProgram.userProgram();textureShaderProgram.setUniforms(modelViewProjectionMatrix, textureId);table.bindData(textureShaderProgram);table.draw();Matrix.setIdentityM(mallet.modelMatrix, 0);Matrix.translateM(mallet.modelMatrix,0, mallet.position.x, mallet.position.y, mallet.position.z);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, mallet.modelMatrix,0);colorShaderProgram.userProgram();colorShaderProgram.setUniforms(modelViewProjectionMatrix, 0f, 0f, 1f);mallet.bindData(colorShaderProgram);mallet.draw();puck.position = puck.position.translate(puck.speedVector);Matrix.setIdentityM(puck.modelMatrix, 0);Matrix.translateM(puck.modelMatrix,0, puck.position.x, puck.position.y, puck.position.z);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, puck.modelMatrix,0);colorShaderProgram.userProgram();colorShaderProgram.setUniforms(modelViewProjectionMatrix, 0f, 1f, 0f);puck.bindData(colorShaderProgram);puck.draw();}

再次运行这个项目应用,当你用木槌击打冰球时,看看会发生什么?

4、最后的策略优化

可能你会发现老问题,冰球飞出了桌子外了,怎么办?为冰球也加上边界测试吧。但这个边界的测试比之前的木槌又复杂了一些。因为,我们要先控制速度向量,我们设定无论何时当冰球碰撞到桌子边缘,它都会从桌子边缘弹开,而且,速度不可能一直不变,我们加入必要的缩小操作,使得速度向量有阻尼效果,最终使得冰球减速停下。

    @Overridepublic void onDrawFrame(GL10 gl10) {GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, table.modelMatrix,0);textureShaderProgram.userProgram();textureShaderProgram.setUniforms(modelViewProjectionMatrix, textureId);table.bindData(textureShaderProgram);table.draw();Matrix.setIdentityM(mallet.modelMatrix, 0);Matrix.translateM(mallet.modelMatrix,0, mallet.position.x, mallet.position.y, mallet.position.z);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, mallet.modelMatrix,0);colorShaderProgram.userProgram();colorShaderProgram.setUniforms(modelViewProjectionMatrix, 0f, 0f, 1f);mallet.bindData(colorShaderProgram);mallet.draw();updatePuckCollisionTest();Matrix.setIdentityM(puck.modelMatrix, 0);Matrix.translateM(puck.modelMatrix,0, puck.position.x, puck.position.y, puck.position.z);Matrix.multiplyMM(modelViewProjectionMatrix,0, viewProjectionMatrix,0, puck.modelMatrix,0);colorShaderProgram.userProgram();colorShaderProgram.setUniforms(modelViewProjectionMatrix, 0f, 1f, 0f);puck.bindData(colorShaderProgram);puck.draw();}// 根据冰球的移动速度,改变冰球位置。还时刻检测冰球的边缘碰撞,当发生边缘碰撞时,速度会比正常状态缩小得更多。private void updatePuckCollisionTest() {puck.position = puck.position.translate(puck.speedVector);if(puck.position.x < Table.leftBound + puck.radius|| puck.position.x > Table.rightBound - puck.radius) {puck.speedVector = new Geometry.Vector(-puck.speedVector.x,puck.speedVector.y, puck.speedVector.z);puck.speedVector = puck.speedVector.scale(0.9f);}if(puck.position.z < Table.farBound + puck.radius|| puck.position.z > Table.nearBound - puck.radius) {puck.speedVector = new Geometry.Vector(puck.speedVector.x,puck.speedVector.y, -puck.speedVector.z);puck.speedVector = puck.speedVector.scale(0.9f);}puck.position = new Geometry.Point(clamp(puck.position.x, Table.leftBound + puck.radius, Table.rightBound - puck.radius),puck.position.y,clamp(puck.position.z, Table.farBound + puck.radius, Table.nearBound - puck.radius));puck.speedVector = puck.speedVector.scale(0.99f);}

现在,效果如何?

小结:我们现在已经到了曲棍球项目的结尾了,花点时间回忆下我们学过的所有内容,因为我们确实走了很长的路。在写系列文章的1~10的时,我自己确实也感受到了温故而知新;在整个过程,我们学习了很多重要概念,首先我们搞清楚了着色器是如何工作的,以及通过学习顶点、矩阵和纹理构建事物,我们甚至还接触了最简单的游戏引擎知识,想想我们只是依靠OpenGL直接在底层走到现在的。

有了这一基础系列的学习,下一系列,我将会开展全景视野方面的总结学习。

项目码云Url:https://github.com/MrZhaozhirong/BlogApp;参考hockey/HockeyActivity

同时就在今天,伟大的物理学家 斯蒂芬·霍金 教授尘归星辰,离开了地球,缅怀一代伟人。

OpenGL.ES在Android上的简单实践:10-曲棍球(拖动物体、碰撞测试)相关推荐

  1. OpenGL.ES在Android上的简单实践:11-全景(索引-深度测试)

    OpenGL.ES在Android上的简单实践:11-全景(正方体-索引-深度测试) 0.全景图要怎么看? What is 全景?可能很多人单看这名字不太清楚.但看到下面的图的时候就噢的一声~瞬间廓然 ...

  2. OpenGL.ES在Android上的简单实践:23-水印录制(FBO离屏渲染,解决透明冲突,画中画)

    OpenGL.ES在Android上的简单实践:23-水印录制(FBO离屏录制,解决透明冲突) 1.水印签名罢工了? 不知道大家有没注意到,之前我们使用MediaCodec录制的视频,水印签名那部分区 ...

  3. OpenGL.ES在Android上的简单实践:21-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 上)

    OpenGL.ES在Android上的简单实践:21-水印录制(MediaCodec输出h264+MediaMuxer合成mp4 上) 1.录制视频需要什么? 在上篇文章,我们已经成功的满足了需求,在 ...

  4. OpenGL.ES在Android上的简单实践:20-水印录制(预览+透明水印 表情 弹幕 gl_blend)

    OpenGL.ES在Android上的简单实践:20-水印录制(预览 gl_blend) 1.继续画出预览帧 紧接着上篇文章,既然是要画出预览帧,按照之前其他项目的架构组成.我们是通过模型FrameR ...

  5. SDL2源码分析之OpenGL ES在windows上的渲染过程

    SDL2源码分析之OpenGL ES在windows上的渲染过程 更新于2018年11月4日. 更新于2018年11月21日. ffmpeg + SDL2实现的简易播放器 ffmpeg和SDL非常强大 ...

  6. 在 OpenGL ES 2.0 上实现视差贴图(Parallax Mapping)

    在 OpenGL ES 2.0 上实现视差贴图(Parallax Mapping) 视差贴图 最近一直在研究如何在我的 iPad 2(只支持 OpenGL ES 2.0, 不支持 3.0) 上实现 视 ...

  7. OpenGL ES for Android 绘制旋转的地球

    No 图 No Code,我们先来欣赏下旋转的地球: 是不是很酷炫,要想绘制出上面酷炫的效果需要3个步骤: 计算球体顶点数据 地球纹理贴图 通过MVP矩阵旋转地球 计算球体顶点数据 我们知道OpenG ...

  8. android opengl流程,【Android OpenGL ES】Android Opengl ES创建流程

    在android 1.0rc2 sdk中,提供了以下包支持Opengl ES 编程: 一.openglES包 android.opengl Class: GLDebugHelper:用于调试OpenG ...

  9. OpenGL ES for Android 绘制立方体

    立方体有6个面,8个顶点,因此绘制立方体其实就是绘制6个面. 顶点shader attribute vec4 a_Position; attribute vec4 a_color; varying v ...

最新文章

  1. centos7安装tomcat8.5.46版本
  2. 读书笔记_CLR.via.c#第十六章_数组
  3. Java Float类floatToIntBits()方法与示例
  4. 挖掘建模-分类与预测-决策树
  5. 简单的SQL语句 DDL
  6. 年假计算器在线_死亡计算器 和 年龄计算器
  7. Android技术专家 高焕堂 推荐这本书
  8. Python 3.x对.CSV数据按任意行、列读取
  9. ortoiseGit--小乌龟git项目
  10. 算法图解第八章笔记与习题(贪婪算法)
  11. opnet matlab联合仿真,OPNET与Matlab联合仿真参数设置
  12. 智能控制器在风机及水泵中的应用
  13. 2021-08-14
  14. pic单片机c语言計數,单片机教程:PIC单片机C语言程序设计(三)
  15. SQL:取 分组后 的 按时间倒序 的前5条数据
  16. 迪文屏程序制作。通讯
  17. 大数据可视化大屏展示
  18. win10家庭版用户实现远程桌面解决办法
  19. web前端知识总结二(css(其他)+移动web网页开发)
  20. 网络安全工程师自主学习计划表(具体到阶段目标)

热门文章

  1. Mathematica绘制常见曲线
  2. VBA操作TXT文档
  3. html+css+js 自定义下拉框+滚动条
  4. Arduino使用 红外接收模块
  5. 不可错过!2019 热门机器学习内容盘点
  6. 三轴加速度计的原理和方法
  7. php生成密钥 对称密码,对称密钥体制和公钥密码体制的特点各是什么
  8. 电脑自动关机是什么原因?如何解决?
  9. 嵌入式基础测试手册——基于NXP iMX6ULL开发板(3)
  10. JavaScript 倒计时动态效果(附带小天使)