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.
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.
With a base application already set up, we can get straight to the creation of the Control pattern:
DeformableControl
that extends AbstractControl
. It needs one private terrain field called terrain
.setSpatial
method and cast Spatial
to fit your terrain field; use terrain = (Terrain) spatial;
to do this.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>();
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)); } } }
terrain.setLocked(false); terrain.adjustHeight(heightPoints, heightValues); terrain.setLocked(true);
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); }
ray
instance that originates from the camera, as shown in the following code:Ray ray = new Ray(cam.getLocation(), cam.getDirection());
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); }
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.
3.144.37.12