【分享】无需物理组件的3d触摸选择 / 摄像机射线检测

先上图:

**适用于伪3d游戏,即游戏所有逻辑判断、碰撞检测都是在2d平面上进行,但物体节点上挂载了3d模型,使用透视摄像机观察画面。很多俯视角游戏、某些卡牌类、横板类都可以是伪3d,既有3d效果的华丽,也从逻辑判定上砍掉一个z轴而节省不少性能。
在3d环境下,触摸屏幕点选物体,一般用射线检测,从3d摄像机向触点发射一条射线,康康命中了什么物体。cocos需要启用物理组件和让节点挂载物理碰撞体才能被射线检测。但我有一个轻量的项目,既不需要物理系统,也不需要3d碰撞体。仅仅为了触摸检测而开物理就太亏了。于是稍微改了下传统射线检测的算法,写了个在轻量级伪3d环境下可用的触摸检测。原理很简单,
首先一个空节点拉满屏幕,接收触摸输入:

触摸板节点挂载脚本,注册触摸按下、移动、离开的监听;获取3d摄像机组件。当触摸按下时,生成一条从摄像机向触点的射线( camera.getRay()函数 ),传递给后面的功能函数。

onLoad() {
// 注册本节点的触摸监听
this.node.on(cc.Node.EventType.TOUCH_START, this.touchOn, this);
this.node.on(cc.Node.EventType.TOUCH_MOVE, this.touchMove, this);
this.node.on(cc.Node.EventType.TOUCH_END, this.touchOff, this);
this.node.on(cc.Node.EventType.TOUCH_CANCEL, this.touchOff, this);
// 获取摄像机组件
this.CCcamera = this.camera.getComponent(cc.Camera);
}
/*** 触摸监控 ***/
touchOn(event) { // 按下
// 获取当前触点的位置
let v2 = event.getLocation();
// 获取一条从摄像机到触点的射线
let ray = this.CCcamera.getRay(v2);
// 传递给功能函数
this.getRayPoint(ray);
},
touchOff() {…}, // 松开
touchMove(event) {…}, // 移动

随后,计算这条射线跟平面的交点。由于要做检测的平面是地面,即z高度为0、法线垂直xy;所以并不需要复杂的立体几何求交公式。
一条射线的参数包含了出发点和角度系数两个vec3。要获取该射线上的一个点,只需代入一个偏移量(即该点离出发点偏移了多远);出发点(x,y,z)+角度系数(x,y,z)×偏移量=射线上的一个点(x,y,z)。


求交的平面是地面,所以交点的z必然为0。因此单独抽出z值,推出偏移量t。然后将t代入原射线方程,得到与平面的交点p。

1)出发.z + 角度.z * t = 0 ----> t = - (出发.z) / 角度.z
2)交点(vec3) = 出发(vec3)+角度(vec3)×t
3)取交点(vec3)的xy,即为在2d平面上的一个点(vec2)。

代码:

// 获取射线与地面的交点
getRayPoint(ray) // 传入一条射线
{
// 求出偏移系数t
let t = (-ray.o.z) / (ray.d.z); // t=-o(z)/d(z)
// 按照t偏移,得到目标点p
let p = ray.o.add(ray.d.mul(t)); // p=o+d*t
},

然后判断这个2d坐标与什么物体碰撞就行了~~我是用一个节点作为“准星”,在上面挂载一个CircleCollider组件,半径调为0;平时禁用准星节点,碰撞发生时启用它 并把位置设置为交点坐标。哦还有,交点坐标是世界坐标;还得先用convertToNodeSpaceAR()转为节点坐标系。如果准星节点发生了碰撞,会通知给挂载的脚本。
(准星不一定要挂载个图片,空节点+圆形碰撞体就行了。)


END.
萌新,有错请指出~

4赞

跟这个是一样的吧

哈哈,是一样滴:joy:我早点看到你的帖子就好了

mark!!!

这个好。虽然找了另一个教程 可以直接判定 ray 和 各种子物体的aabb是否相交
但其实我还是需要一个p点的。
因为。比如我有个地图很大900*900 那我肯定想从屏幕触摸点。得到这个地图交点(例如相交点在1/3) 地图的坐标点为(0,0) 相交点p 我希望是300 300.
那其他操作例如 放只小羊 在我刚刚的触摸点。(这很常见的业务)
补充其实creator2大家会是因为有坐标转换函数,creator3 因为有相机的概念。不能照搬。(不知道我理解对不对,多多指教)

技术细节求教下
除法得到的是number
// 求出偏移系数t
let t = (-ray.o.z) / (ray.d.z); // t=-o(z)/d(z)
// 按照t偏移,得到目标点p
let p = ray.o.add(ray.d.mul(t)); // p=o+d*t
新版本中vet3 没有mul 只有multiply
而multiply 的参数需要一个vert3 怎么搞

自己找到答案了。应该用multiplyScalar 函数

我也写了类似的算法,只不过我是z=0平面,竖着的,直线用的两点式:(我这里摄影机是一点,worldPos是一点)

point3DInZ0: function (worldPos) {
        let o1 = this.camera3dNode.parent.convertToWorldSpaceAR(cc.v3(this.camera3dNode.position));
        let o2 = cc.v3(worldPos);
        let Z = -o1.z / (o2.z - o1.z);
        let x = Z * (o2.x - o1.x) + o1.x;
        let y = Z * (o2.y - o1.y) + o1.y;
        return cc.v3(x, y, 0);
    },

当然了,还有一个逆算法:我这里的两个平面有所偏移,是交线平行于x轴的两个平面
这是已知竖直的平面上的点,求映射到斜着的平面上的点

pointZ0In3D: function (worldPos) {

        let yOffset = Global.UNG.Temp._yOffset_fromExpGame;

        let t = Global.UNG.Temp._t_fromExpGame;

        let T = 1 / Math.tan(t * Math.PI / 180);

        let o1 = this.camera3dNode.parent.convertToWorldSpaceAR(cc.v3(this.camera3dNode.position));

        let o2 = cc.v3(worldPos);

        let { x1, y1, z1 } = { x1: o1.x, y1: o1.y, z1: o1.z };

        let { x2, y2, z2 } = { x2: o2.x, y2: o2.y, z2: o2.z };

        let Kz = (y2 - y1) / (z2 - z1);

        let z = ((Kz * z1) - y1 + yOffset) / (Kz - T);

        let y = T * z + yOffset;

        let x = (y - y1) * (x2 - x1) / (y2 - y1) + x1;

        return cc.v3(x, y, z);

    },