2
头图

github address: https://github.com/hua1995116/Fly-Three.js

Hello everyone, I am Qiufeng. Following the previous article " Three.js Series: The First/Three Person View in Games ", what I want to share with you today is to make an ocean ball pool.

Have you ever seen Ocean Ball? It is the kind of ball that is very popular with children in the mall, and I want to jump in it when I see it.

图片

I just want to make an ocean ball pool, and then take everyone to learn the physics engine in Three.js by the way.

So let's get started. To realize an ocean ball pool, there must be "balls" first.

So let's take you to implement a small ball first, and it is very simple to define a small ball in Three.js. Because Three.js provides us with a very rich geometric shape API, there are about a dozen.

图片

The provided geometry happens to have the sphere we need, and the spherical API is called SphereGeometry.

 SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)

This API has a total of 7 parameters, but we only need to use the first 3 parameters, and the latter ones do not need to be managed for the time being.

The meaning of Radius is very simple. It is the radius. To put it bluntly, it is to set the size of the ball. First, we set the size of the ball to 0.5, and then the widthSegments and heightSegments. It looks more delicate, but the consequence of refinement is that the performance consumption is greater, the default value of widthSegments is 32, the default value of heightSegments is 16, we can set 20, 20

 const sphereGeometry = new THREE.SphereGeometry(0.5, 20, 20);

This is very simple. Although the ball has a shape, we still have to set a material for the ball. The material is similar to the material in our real life, not just a spherical shape, such as marbles made of glass. , tennis balls with rubber material, etc., different materials will reflect light differently and look different. In Three.js, we set a standard physical material MeshStandardMaterial , which can set the metalness and roughness, reflect the light, and then set the color of the ball to red,

 const sphereMaterial = new THREE.MeshStandardMaterial({
  color: '#ff0000'
});
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);

scene.add(mesh);

Then we added it to our scene, emmm, and it looked like a black patch.

image.png

"God said there should be light, so there is light", it is normal to be dark, because there is no light in our scene, the meaning is very simple, when the light is turned off at night, of course, you can't see your fingers. So we added two lights to the scene, one ambient light and one direct light. Lighting is not the focus of this article, so it will not be described. Just remember, "It's getting dark, turn on the lights"

 // Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)

// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(2, 2, -1)
scene.add(directionalLight)

图片

Ok! Now the ball finally shows what it looks like.

A static ocean ball is definitely not interesting, we need to make it move, so we need to add a physics engine to it. With the physics engine, the ball will look like it does in real life. It has gravity. It will fall freely at high altitudes. Objects of different materials will have different reactions when they fall to the ground. The tennis ball will bounce and fall again. , the shot put is stationary when it hits the ground.

Commonly used 3d physics engines are Physijs, Ammo.js, Cannon.js and Oimo.js, etc. Here we are using Cannon.js

There are a lot of 3d physics effects on the Cannon.js official website. For details, see his official website https://pmndrs.github.io/cannon-es/

图片

Import Cannon.js

 import * as CANNON from 'https://cdn.jsdelivr.net/npm/cannon-es@0.19.0/dist/cannon-es.js';

First create a physical world and set the gravity coefficient to 9.8

 const world = new CANNON.World();

world.gravity.set(0, -9.82, 0);

Create a ball in the physical world that corresponds to our Three.js one-to-one. The only difference is that you need to set the mass, which is the weight of the ball.

 const shape = new CANNON.Sphere(0.5);

const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
});

world.addBody(body);

Then we modify our rendering logic, we need to make the rendering of each frame correspond to the physical world.

 + const clock = new THREE.Clock();
+ let oldElapsedTime = 0;

const tick = () => {
+   const elapsedTime = clock.getElapsedTime()
+   const deltaTime = elapsedTime - oldElapsedTime;
+   oldElapsedTime = elapsedTime;

+   world.step(1 / 60, deltaTime, 3);

    controls.update();

    renderer.render(scene, camera)

    window.requestAnimationFrame(tick)
}

tick();

But found that our ball did not move, because we did not bind the relationship with the Three.js ball in the physical world.

 const tick = () => {
 ...
+ mesh.position.copy(body.position);
 ...
}

Let's see how it looks now.

图片

The ball already has physical properties and is in free fall~ But since there is no ground, the ball falls into the endless abyss. We need to set a floor to let the ball fall on a flat surface.

Create the ground in Three.js, which is mainly used here PlaneGeometry it has 4 parameters

 PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)

Similar to before, we only need to pay attention to the first two parameters, that is, the width and height of the plane. Since the plane is the plane of the xy axis by default, since Three.js uses the right-handed coordinate system by default, the corresponding rotation is also the right-hand rule, so counterclockwise is positive, clockwise is negative, and our plane needs to be rotated 90° clockwise, so it is -PI/2

 const planeGeometry = new THREE.PlaneGeometry(20, 20);
const planeMaterial = new THREE.MeshStandardMaterial({
    color: '#777777',
});

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI * 0.5;
scene.add(plane);

Then continue to bind the plane's physics engine, which is basically the same as Three.js, but the API name is different

 const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5);
world.addBody(floorBody);

Let's see the effect:

图片

But this effect seems to be the effect of a shot put on the ground, without any rebound and other effects. In order to make the ball not fall directly on the ground like a shot put, we need to add an elastic coefficient to the ball.

 const defaultMaterial = new CANNON.Material("default");

const defaultContactMaterial = new CANNON.ContactMaterial(
    defaultMaterial,
    defaultMaterial,
    {
        restitution: 0.4,
    }
);
world.addContactMaterial(defaultContactMaterial);
world.defaultContactMaterial = defaultContactMaterial;

...
const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
+   material: defaultMaterial,
}); 
...

Check the effect:

图片

Of course, the ocean ball pool cannot have only one ball. We need to have many, many balls. Next, we will implement the case of multiple balls. In order to generate multiple balls, we need to write a random ball generator.

 const objectsToUpdate = [];
const createSphere = (radius, position) => {
 const sphereMaterial = new THREE.MeshStandardMaterial({
     metalness: 0.3,
     roughness: 0.4,
     color: Math.random() * 0xffffff
 });
 const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
 mesh.scale.set(radius, radius, radius);
 mesh.castShadow = true;
 mesh.position.copy(position);
 scene.add(mesh);
 
 const shape = new CANNON.Sphere(radius * 0.5);
 const body = new CANNON.Body({
     mass: 1,
     position: new CANNON.Vec3(0, 3, 0),
     shape: shape,
     material: defaultMaterial,
 });
 body.position.copy(position);
 
 world.addBody(body);
 
 objectsToUpdate.push({
     mesh,
     body,
 });
};

The above is just a function encapsulation of the code we wrote before, and the color of the ball is random. We expose the two parameters of the position of the ball and the size of the ball.

Finally, we need to modify the update logic, because we need to modify the position information of each ball every frame.

 const tick = () => {
...
for (const object of objectsToUpdate) {
    object.mesh.position.copy(object.body.position);
    object.mesh.quaternion.copy(object.body.quaternion);
}
...
}

Next, let's write a click event, which can generate 100 ocean balls when the screen is clicked.

 window.addEventListener('click', () => {

  for (let i = 0; i < 100; i++) {
      createSphere(1, {
          x: (Math.random() - 0.5) * 10,
          y: 10,
          z: (Math.random() - 0.5) * 10,
      });
  }
}, false);

Check out the following effects:

图片

The initial effect has been achieved. Since our pool has only one plane at the bottom and no walls are set, the balls are scattered around. So it is easy to think that we need to build 4 walls, because the difference between the wall and the bottom plane is that it has thickness, it is not a simple surface, so we need to use a new shape BoxGeometry , It also has a total of 7 parameters, but we only need to pay attention to the first 3, which correspond to the length, width and height.

 BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)

Now let's build a wall with a length of 20, a width of 5, and a thickness of 0.1.

 const box = new THREE.BoxGeometry(20, 5, 0.1);
const boxMaterial = new THREE.MeshStandardMaterial({
    color: '#777777',
    metalness: 0.3,
    roughness: 0.4,
});

const box = new THREE.Mesh(box, boxMaterial);
box.position.set(0, 2.5, -10);
scene.add(box)

Now it looks like this:

图片

Then we "paint the scoop according to the gourd" to complete the remaining 3 walls:

图片

Untitled

Then we also added a physics engine to our wall, so that when the ball touched it, it seemed to actually hit the wall instead of penetrating the wall.

 const halfExtents = new CANNON.Vec3(20, 5, 0.1)
const boxShape = new CANNON.Box(halfExtents)
const boxBody1 = new CANNON.Body({
    mass: 0,
    material: defaultMaterial,
    shape: boxShape,
})

boxBody1.position.set(0, 2.5, -10);

world.addBody(boxBody1);
...
boxBody2
boxBody3
boxBody4

View the effect

图片

Harvest a pot full of ocean balls

图片

You're done!

To summarize what we have learned in this issue, we use SphereGeometry, PlaneGeometry, and BoxGeometry together, and then learn the binding of Three.js geometry to the physics engine cannon.js, so that the ball has physical characteristics.

The main steps are

  • define ball
  • Introduce physics engine
  • Combining Three.js with a physics engine
  • Generate random balls
  • define wall

Well, that's all for this chapter, see you in the next chapter.

github address: https://github.com/hua1995116/Fly-Three.js

The first address of the series serialization:

  1. Three.js series: Build an ocean ball pool to learn the physics engine
  2. Three.js series: first and third person perspective in games

程序员秋风
3k 声望3.9k 粉丝

JavaScript开发爱好者,全栈工程师。