Deforming a terrain in real time

A deformable terrain is something that can have a serious effect on the gameplay, or it can simply be a cosmetic bonus. It can be used for impact craters or games that require excavation.

We'll base the deformation around the Control class pattern as this allows us to offset the code in a manageable and reusable way. The recipe will trigger the deformation based on a mouse click, and it will use a ray to detect the collision point.

Getting ready

To get up and running quickly, unless there already is an application to apply this to, TestTerrain.java from the jMonkeyEngine's test cases will provide a good start for what we need. This example will expand on the code provided in that application, but it should work perfectly well with any terrain-based application.

How to do it...

With a base application already set up, we can get straight to the creation of the Control pattern:

  1. Create a new class called DeformableControl that extends AbstractControl. It needs one private terrain field called terrain.
  2. Override the setSpatial method and cast Spatial to fit your terrain field; use terrain = (Terrain) spatial; to do this.
  3. Create a method called deform that takes the 2D location, the radius of the deformation, and the force as an input. Also, declare two lists that we'll use in the heightPoints and heightValues methods, as follows:
    public void deform(Vector2f location, int radius, float force) {
      List<Vector2f> heightPoints = new ArrayList<Vector2f>();
      List<Float> heightValues = new ArrayList<Float>();
  4. Now, we should create a nested for loop statement where we can iterate from -radius to +radius in both x and y (z to be correct). See how far from the center the point is and calculate the height to change at that location. The decrease of the force of the impact will be proportional to how far out it is from the center. Then, save the point in the heightPoints list and the new height in the heightValues list as follows:
    for(int x = -radius; x < radius; x++){
      for(int y = -radius; y < radius; y++){
        Vector2f terrainPoint = new Vector2f(location.x + x, location.y + y);
        float distance = location.distance(terrainPoint);
        if(distance < radius){
          float impact = force * (1 - distance / radius) ;
          float height = terrain.getHeight(terrainPoint);
          heightPoints.add(terrainPoint);
          heightValues.add(Math.max(-impact, -height));
        }
      }
    }
  5. To wrap up the method, we need to apply the new heights. First, unlock the terrain and then lock it again as follows:
    terrain.setLocked(false);
    terrain.adjustHeight(heightPoints, heightValues);
    terrain.setLocked(true);
  6. Since we normally work with 3D vectors rather than 2D vectors, it can be a good idea to also create a convenience method called deform, which takes Vector3f as the input. It converts this input to Vector2f and in turn calls the other deform method as follows:
    public void deform(Vector3f location, int radius, float force){
      Vector2f pos2D = new Vector2f((int)location.x, (int)location.z);
      deform(pos2D, radius, force);
    }
  7. Now, trigger the deformation from a method in our application. Firstly, it should create a new ray instance that originates from the camera, as shown in the following code:
    Ray ray = new Ray(cam.getLocation(), cam.getDirection());
  8. Next, create a new CollisionsResults object and check whether the ray intersects the terrain. If there is a collision, call deform on the terrain's DeformableControl object by supplying the contactPoint parameter of the collision as follows:
    CollisionResults cr = new CollisionResults();
    terrain.collideWith(ray, cr);
    CollisionResult collision = cr.getClosestCollision();
    if(collision != null){
      terrain.getControl(DeformableControl.class).deform(coll.getContactPoint(), 30, 30f);
    }

How it works...

When deforming the terrain, we collect all the points we want to modify and the new heights in lists; then, we collectively update the terrain based on them. There is an adjustHeight method to update a single point as well, but it is assumed that it's faster using a list.

Locking the terrain means faster rendering. Whether to lock the terrain or not depends on the implementation. If it is a terrain that is changed with every frame, it probably doesn't need to be locked. On the other hand, if it changes only occasionally, it should probably be locked.

The formula that is used to calculate the change in height is deltaHeight = force * (1 - distance / radius). This means that the change in height will be highest when it is closest to the center; it will then fall off linearly as the distance increases and we get closer to the edge of the radius. A variation worth exploring is to use the root with deltaHeight = force * FastMath.sqrt(1 - distance / radius) instead. This will provide a rounder shape to the terrain.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.37.12