three.js + cannon-es 实现 3D 博饼

1,684 阅读4分钟

前言

本文将使用 three.js + cannon-es 构建一个 3D 的博饼 小游戏,在这里你能够学到如何通过 three.js 构建一个骰子🎲,如何通过 cannon-es 为骰子添加物理动作,下面就让我们开始介绍。

构造骰子

首先骰子是一个正方体,但是又有一点不同,不同在于他的边角都是圆的,并且每个面有不同个数的凹陷。

构建圆边角的正方体

首先让我们来构建圆边角的正方体,你可以通过 RoundedBoxGeometry类 来扩展 BoxGeometry 类,你可以在 examples/jsm/geometries/ 文件夹下找到这个类文件。

首先我们要清楚,要构建一个物体也就是网格模型Mesh,需要物体的形状(Geometry)以及物体的外观(Material),上面的 RoundedBoxGeometry 类帮我们解决了形状的问题,

image-20230920104736702.png

在源码中可以发现,我们可以传入 5 个 参数,其中前三个参数对应就是物体的长宽高,第四个参数对应就是在三个方向的分的段数,重点在于第五个参数,决定了正方体的圆角

我们就可以通过 const geometry = new RoundedBoxGeometry(1, 1, 1, 3, 0.1); 来构建一个带有圆角的正方体形状

为每一个面上色

接着我们来创建我们需要的材质,你可以用最简单的单一颜色材质就可以看到之前创建的形状。

//创建一个材质对象Material
const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,//0xff0000设置材质颜色为红色
}); 

image-20230920134545072.png

然后我们需要用到 THREE.MeshBasicMaterial 来为每个面创建材质,这个函数可以传入一个对象,对象的 map 属性为一个 canvas 对象,然后这个面就会展示这个 canvas 画的内容,然后我们就可以通过 canvas 来画出每一个面的点。

 var materials = [
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(1)) }), // right
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(6)) }), // left
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(3)) }), // top
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(4)) }), // bottom
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(5)) }), // back
	new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(getCanvas(2)) }), // front
];

然后让我们将这个骰子添加到场景当中,并且我这里使用了 OrbitControls 方便观察每一个面的状况

为骰子添加动画

three.js 不过是帮我们创建好了 3D 模型,这其中不包括一些物理引擎或者其他的来帮助我们实现动画。

所以我们需要通过 cannon-es 来构建物理相关的动画,实现扔骰子的过程。

在这个过程当中,我们需要实现

  1. 骰子是一个刚体,它不应该发生形变
  2. 用一个平面来代表地面
  3. 碰撞检测,骰子与地面进行碰撞以及之后的弹跳逻辑
  4. 投掷骰子使得它们落在地面上

首先我们利用 new CANNON.World({}) 来创造一个物理世界,并且把我们的骰子放入这个物理世界当中,上面的新建骰子模型的方法我们可以抽出来,用于循环创建多个骰子。

const diceArray = [];

diceMesh = createDiceMesh();

for (let i = 0; i < params.numberOfDice; i++) {
    diceArray.push(createDice());
}

function createDice() {
    const mesh = diceMesh.clone();
    scene.add(mesh);

    const body = new CANNON.Body({
        mass: 1,
        shape: new CANNON.Box(new CANNON.Vec3(.5, .5, .5)),
    });
    physicsWorld.addBody(body);

    return {mesh, body};
}

然后我们把创建好的骰子先放到物理世界中去

 diceArray.forEach((d, dIdx) => {
	d.body.position = new CANNON.Vec3(3, dIdx * 1.5, 0); 
	d.mesh.position.copy(d.body.position);
});

这时候我们就可以见到固定在天上的骰子,然后我们来为骰子创建一个地面,这样等下骰子才有地方可以碰撞。

function createFloor() {
    
    // Three.js (visible) object
    const floor = new THREE.Mesh(
        new THREE.PlaneGeometry(1000, 1000),
        new THREE.ShadowMaterial({
            opacity: .1
        })
    )
    floor.receiveShadow = true;
    floor.position.y = -7;
    floor.quaternion.setFromAxisAngle(new THREE.Vector3(-1, 0, 0), Math.PI * .5);
    scene.add(floor);

    // Cannon-es (physical) object
    const floorBody = new CANNON.Body({
        type: CANNON.Body.STATIC,
        shape: new CANNON.Plane(),
    });
    floorBody.position.copy(floor.position);
    floorBody.quaternion.copy(floor.quaternion);
    physicsWorld.addBody(floorBody);
}

将创建好的地面丢入物理世界当中,并且确定好二者之间的初始距离,然后我们来为这个事件添加重力。

let physicsWorld = new CANNON.World({
    allowSleep: true,
    gravity: new CANNON.Vec3(0, -50, 0),
})

这时候骰子就会开始下落了,但是因为只有一个下落的力,所有就仅仅只是下落而已。

动画3.gif

通过修改弹力,初始化的时候施加一个随即方向的力,以及在每次渲染的时候需要去更新骰子,这时候我们就可以得到一个比较灵动的扔骰子的动作了。

动画4.gif

最后我们可以设定一个按钮,点击这个按钮之后,就可以重置骰子的状态,重新投掷

function throwDice () {
  diceArray.forEach((d, dIdx) => {
	d.body.position = new CANNON.Vec3(4, dIdx * 1.5 + 5, 0);
	d.mesh.position.copy(d.body.position);
	d.mesh.rotation.set(1 * Math.PI * Math.random(), 0, 2 * Math.PI * Math.random())
	d.body.quaternion.copy(d.mesh.quaternion);
	const force = 1 + 5 * Math.random();
	d.body.applyImpulse(
		new CANNON.Vec3(-force, force, 0)
	);
  });
}

综上,我们就能够得到一个简单的扔骰子小游戏了。

参考

GitHub - uuuulala/Threejs-rolling-dice-tutorial