简介
在3D游戏开发中,有时候我们需要实现一个功能:玩家通过鼠标点击或触摸屏幕来选择场景中的物体,并对选中的物体进行高亮显示,如下图:

这种交互方式可以增强游戏的沉浸感,使玩家更好地理解和控制游戏世界。要实现这个功能,通常使用射线投射
来对3D场景中的物体进行碰撞检测。
原理
射线投射(Raycasting)在3D游戏和图形编程中是一种常见的技术,用于解决各种问题,包括(但不限于)碰撞检测,物体选中、物体选取,人工智能视觉等。
射线投射的基本思想是:你有一条从特定点开始沿特定方向的射线(可以想象成一条无穷长的直线),并且你想知道这条射线是否与3D空间中的某个物体相交。
这个过程的实现可以分为以下几个步骤:
- 定义射线:射线有一个起点和一个方向。在这个问题中,起点通常是摄像机的位置,方向是从摄像机指向鼠标点击(或触摸)位置的方向。这个方向可以通过将鼠标点击的屏幕坐标转换为3D世界坐标得到。这个转换过程需要使用摄像机的视角和视野等参数。
- 检测碰撞:然后你需要检查这条射线是否与场景中的任何物体的碰撞体(Collider)相交。这可以通过检查射线与每个物体的碰撞体的交点来实现。如果存在交点,那么就可以说射线与物体相交。
- 处理结果:如果射线与多个物体相交,那么你需要选择一个进行操作。通常选择的是最近的物体(也就是射线起点与交点距离最短的物体)。
这个过程的复杂性取决于场景中物体的数量和复杂性。对于简单的物体(如球或盒子),检测射线与物体的交点是相对简单的。对于更复杂的物体,可能需要使用更复杂的算法,或者使用简化的碰撞体(例如,使用物体的外接盒作为碰撞体)。
大多数现代3D游戏引擎都提供了射线投射的内置支持,可以很方便地进行这个过程。例如,在Unity中,你可以使用Physics.Raycast
函数来进行射线投射。在Three.js中,你可以使用THREE.Raycaster
对象来进行射线投射。而CocosCreator(3.x版本)中使用PhysicsSystem.instance.raycastClosest
来做实现。
实现
在场景中,添加几个3D物体到一个平面上,如图
挂载类Game
代码如下:
typescript
import { _decorator, Component, CameraComponent, systemEvent, SystemEventType, Touch, PhysicsSystem, geometry, LabelComponent, MeshRenderer, Material } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Game')
export class Game extends Component {
@property({displayName: "3D相机", type: CameraComponent})
camera: CameraComponent = null!;
@property({displayName: "文字", tooltip: "文字", type: LabelComponent})
label: LabelComponent = null!;
@property({displayName: "描边材质", tooltip: "描边材质", type: Material})
outline: Material = null!;
@property({displayName: "默认材质", tooltip: "默认材质", type: Material})
default: Material = null!;
// 上一个选中的结果
last: MeshRenderer = null!;
onLoad () {
this.label.string = "请点击屏幕";
systemEvent.on(SystemEventType.TOUCH_START, (e: Touch) => {
// 获取触摸点并且创建射线
let pos = e.getLocation();
let ray = this.camera.screenPointToRay(pos.x, pos.y);
// 传入射线
this.rayCollisionDetection(ray);
}, this);
}
rayCollisionDetection (ray: geometry.Ray) {
// 如果选中了节点
if (PhysicsSystem.instance.raycastClosest(ray) == true) {
// 获取射线最短的检测结果
var node = PhysicsSystem.instance.raycastClosestResult;
// 获取名字
let name = node.collider.name;
// 根据不同的名字进行不同的判断
if (name == "Cylinder<CylinderCollider>") {
this.label.string = "点击到了物体:圆柱";
} else if (name == "Cube<BoxCollider>") {
this.label.string = "点击到了物体:立方体";
} else if (name == "Capsule<CapsuleCollider>") {
this.label.string = "点击到了物体:胶囊";
} else if (name == "Cone<ConeCollider>") {
this.label.string = "点击到了物体:圆锥";
} else if (name == "Sphere<SphereCollider>") {
this.label.string = "点击到了物体:球";
} else if (name == "Torus<MeshCollider>") {
this.label.string = "点击到了物体:圆环";
} else {
this.label.string = "点击到了物体:" + node.collider.name;
}
// 设置检测结果的材质为红色描边
node.collider.node.getComponent(MeshRenderer)!.material = this.outline;
// 如果不是第一次选中并本次选中和上次不同
if (this.last != null && node.collider.node.getComponent(MeshRenderer) != this.last) {
// 设置为默认材质
this.last.material = this.default;
}
// 设置上次选中的结果
this.last = node.collider.node.getComponent(MeshRenderer)!;
} else {
// 没选中任何物体设置上次选中结果材质为默认
this.label.string = "没点到任何物体";
if (this.last != null) {
this.last.material = this.default;
}
}
}
}
这段代码的核心原理主要涉及到两个方面:射线投射(Raycasting)和材质切换。
射线投射(Raycasting)
射线投射是3D图形和游戏开发中常用的技术。它基于一个简单的概念:从一个点发出一条射线,看这条射线是否和场景中的物体相交。
在这段代码中,射线的起点是3D相机,方向是从相机指向屏幕上的触摸点。通过camera.screenPointToRay(pos.x, pos.y)
这行代码,我们将2D屏幕坐标转换为3D空间中的一条射线。然后,我们使用PhysicsSystem.instance.raycastClosest(ray)
来检测这条射线是否与场景中的任何物体相交。
如果射线与物体相交,PhysicsSystem.instance.raycastClosest(ray)
将返回true
,并且我们可以通过PhysicsSystem.instance.raycastClosestResult
获取到射线碰撞的最近的物体。这就是我们选中的物体。
材质切换
材质是定义物体表面外观的属性,包括颜色、纹理、光照效果等。在这段代码中,我们定义了两种材质:默认材质和描边材质。默认材质是物体正常的外观,描边材质则是物体被选中时的外观。
当我们选中一个物体时,我们会获取到物体的MeshRenderer组件,然后将它的材质切换为描边材质。这样就可以实现物体被选中时的高亮效果。当我们选中另一个物体时,我们会将上一次选中的物体的材质切换回默认材质。
通过这两个步骤,我们就可以实现在3D游戏中通过点击或触摸来选中物体,并对选中的物体进行高亮显示的功能。