The Dynamic Terrain is an extension that provides a way to display a large terrain dynamically morphed from a map of 3D data.
The dynamic terrain is basically a standard BJS mesh, actually a ribbon. It's linked to a camera and moves with it along the World X and Z axes. It's passed a set of geographic data what are each simply 3D coordinates (x, y, z) in the World. This set of data is called the map. According to the current camera position in the World, the dynamic terrain morphs to depict the map at this location.
Just download the javascript file dynamicTerrain.js
(or, recommended, the minified version dynamicTerrain.min.js
) from the BabylonJS extension repository folder DynamicTerrain/dist
: https://github.com/BabylonJS/Extensions/tree/master/DynamicTerrain/dist
Then in your code, declare this script in a html tag after the script tag declaring Babylon.js :
<script src="babylon.js"></script>
<script src="dynamicTerrain.min.js"></script>
The dynamic terrain is a mesh that morphs on a logical data map.
This map is a simple flat array of successive 3D coordinates (x, y, z) as floats.
It can be as huge as you need, as long as you've enough memory. This won't be rendered anyway.
The map must be passed to the dynamic terrain constructor as well as the number of subdivisions on the map width and height.
var mapCoords = [some_big_flat_array_of_coordinates];
var mapWidthPointNb = 2000; // 2000 points in the map width
var mapHeightPointNb = 1000; // 1000 points in the map height
var terrainSub = 100; // the terrain wil be 100x100 vertices only
var mapParams = {
mapData: mapCoords,
mapSubX: mapWidthPointNb,
mapSubZ: mapHeightPointNb,
terrainSub: terrainSub
};
var terrain = new BABYLON.DynamicTerrain("terrain", mapParams, scene);
var terrainMesh = terrain.mesh;
terrainMesh.diffuseTexture = myNiceTexture;
Some documented examples are here :
https://github.com/BabylonJS/Extensions/tree/master/DynamicTerrain/documentation/dynamicTerrainExamples.md
The first thing we need to create a dynamic terrain is a data map.
The data map is a simple flat array of successive 3D coordinates (x, y, z) in the World.
It's defined by the number of points on the map width, calledmapSubX
by the dynamic terrain, and the number of points on the map height, called mapSubZ
.
The dynamic terrain imposes some constraints to the map :
What does this mean ?
If we call P[i, j]
the point P at the row j
on the map height and at the column i
on the map width, this means that :
j
in the map, P[0, j].x
is lower than P[1, j].x
, what is lower than P[2, j].x
, etci
in the map, P[i, 0].z
is lower than P[i, 1].z
, what is lower than P[i, 2].z
, etcExample :
Here, we populate a big Float32Array
with successive 3D float coordinates.
We use a simplex function from a third party library to set each point altitude.
This array is the data map. It's defined by 1000 points on its width and 800 points on its height.
The distance between the points is constant on the width and is different from the constant distance between the points on the height.
var mapSubX = 1000; // map number of points on the width
var mapSubZ = 800; // map number of points on the height
var seed = 0.3; // set the noise seed
noise.seed(seed); // generate the simplex noise, don't care about this
var mapData = new Float32Array(mapSubX * mapSubZ * 3); // x3 because 3 values per point : x, y, z
for (var l = 0; l < mapSubZ; l++) { // loop on height points
for (var w = 0; w < mapSubX; w++) { // loop on width points
var x = (w - mapSubX * 0.5) * 5.0; // distance inter-points = 5 on the width
var z = (l - mapSubZ * 0.5) * 2.0; // distance inter-points = 2 on the width
var y = noise.simplex2(x, z); // altitude
mapData[3 * (l * mapSubX + w)] = x;
mapData[3 * (l * mapSubX + w) + 1] = y;
mapData[3 * (l * mapSubX + w) + 2] = z;
}
}
Playground Example Dynamic Map -
In this example, the data map is generated in a Float32Array. The very useful library perlin.js is used to compute the altitude of each point with a simplex2 noise function.
In order to better understand how this map is generated, we use it as a ribbon mesh geometry here. In this example, the ribbon is thus a really big mesh (1000 x 800 = 800K vertices !). So you shouldn't try to render so big meshes in your scene if you want to keep a decent framerate. Moreover, remember that the logical map could also be bigger than 800K points.
Once we've got the data map, we can create the dynamic terrain.
var terrainSub = 100; // 100 terrain subdivisions
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
terrainSub: terrainSub // how many terrain subdivisions wanted
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
Playground Example Dynamic Terrain -
The dynamic terrain is the green mesh flying on the data map.
We can notice that the green terrain is linked to the scene active camera on its center and moves with it when we zoom in or out.
Actually, the terrain adjusts itself automatically to the exact next points of the map as the camera moves over it.
The terrain is defined by its number of subdivisions what is the same on both its axes X and Z.
It's better to choose a multiple of 2 as the number of terrain subdivisions.
Once created, this number never changes, neither the terrain number of vertices, but only its shape.
This means the computation charge to update the terrain is always constant and depends only on this subdivision value.
The terrain is a logical object providing several features. It embeds a BJS mesh accessible with the property .mesh
:
var terrainMesh : terrain.mesh;
terrain.Mesh.diffuseTexture = myTerrainTexture;
Although a data map must be passed at construction time, this map can be changed thereafter as well as the properties .mapSubX
and mapSubZ
.
terrain.mapData = map2;
terrain.mapSubX = mapSubX2;
terrain.mapSubZ = mapSubZ2;
This is useful if we have to download dynamically new chuncks of data as the camera moves in the World.
Example :
// change the terrain map on the fly
if (camera.position.z > someLimit) {
terrain.mapData = map2;
}
If the map data aren't updated or if a new data array isn't passed to the terrain when it reaches the map edges, then the terrain goes on moving as if the current map where repeated on the current axis.
This means that from the terrain perspective and unless we give it a speficic behavior regarding the map bounds, the data map is infinitely repeated or tiled in every direction of the ground.
In short, by default, the terrain sees the map as infinite.
LOD is an acronym for Level Of Detail.
It's a feature allowing to reduce the rendering precision of some mesh when it's far away from the camera in order to lower the necessary computation : the less vertices, the less CPU/GPU needed.
The dynamic terrain provides also a LOD feature but in a different way : the terrain number of vertices always keeps constant but only the part of data map covered by the terrain changes.
By default, one terrain quad fits one map quad.
This factor can be modified with the property .initialLOD
(equal to 1, by default) at any time.
Examples :
The default initial LOD is 1, so 1 terrain quad is 1 map quad
Playground Example - 1 Terrain Quad 1 Map Quad -
The initial LOD is set to 10, so 1 terrain quad is now 10x10 map quads (10 on each axis)
Playground Example - 1 Terrain Quad 1 Map Quad -
In consequence, the terrain mesh is far bigger, far less detailed regarding to the map data, but keeps the same amount of vertices (100 x 100).
Setting an initial LOD to 10 is probably not a pertinent value, it's done only in the purpose of the explanation.
In brief, the initial LOD value is the number of map quads on each axis, X and Z, per terrain quad.
Back to the terrain with the default initial LOD value.
We can notice that when the camera is at some high altitude the green terrain seems far away, quite little in the screen area, as this is the common behavior : distant things appear tinier.
Playground Example Distant -
However we don't expect that, when getting in higher altitude, the ground would get tinier : it becomes less detailed to our eyes and we can see a larger area of the ground in the same time.
The dynamic terrain provides a way to do this by increasing the LOD factor with the camera altitude (or any other behavior we may want like changing the LOD with the camera speed instead).
We just have to overwrite the method updateCameraLOD(camera)
and make it return an integer that will be added to the current LOD value.
// Terrain camera LOD : custom function
terrain.updateCameraLOD = function(terrainCamera) {
// LOD value increases with camera altitude
var camLOD = Math.abs((terrainCamera.globalPosition.y / 16.0)|0);
return camLOD;
};
Playground Example Camera LOD -
In this example, the LOD value is incremented by 1 each time the altitude is +16 higher.
If we get the camera higher by zooming out when looking at the ground, we can see that the terrain size increases since there are less details.
This function is passed the object camera linked to the terrain and must return a positive integer or zero. By default, zero is return.
This function is called on each terrain update.
Nevertheless, we can set this value at any time with the property .cameraLODCorrection
.
Example :
terrain.cameraLODCorrection = 3; // adds +3 to the initial LOD
In this example, the camera LOD correction value of 3, forces the global LOD factor to be 4 : 3 (camera) + 1 (initial value). This means each terrain quad is now 16 (4 x 4) map quads.
In general, we don't need to set this value manually. It's better to update it automatically with the method updateCameraLod(camera)
, so the property is rather read than set.
This feature is useful only when the expected camera movements can get the terrain very distant, so too tiny, in the field of view.
It's not really necessary to use it if the terrain keeps quite the same size in the fied of view.
Example : a character walking on the terrain ground.
The global LOD factor is the current sum of the initial value and the current camera LOD correction value.
As said before, it's the current factor of the number of map quad per axis in each terrain quad.
It's readable with the property .LODValue
.
var lod = terrain.LODValue;
It's a positive integer (>= 1).
Let's simply remember that the bigger the LOD value, the lower the terrain details.
The perimetric LOD is the LOD in the distance around the terrain perimeter.
When our camera is close enough to the ground and looking at distant things in the landscape, we expect that these things don't require too many vertices to be rendered, because they are far from us and don't need to be as detailed as near objects.
Let's get of the map rendering and let's create a smaller terrain of 20 subdivisions only :
Playground Example Smaller Terrain -
The camera is located high in altitude in order to understand better how to set the perimetric LOD.
The property to change the perimetric LOD is .LODLimits
. It's an array of integers (or an empty array, by default).
Let's set a first limit to 4 :
terrain.LODLimits = [4];
Playground Example LOD Limits [4] -
How is now the terrain after a forced update (note : the terrain automatically update with the camera movement on X or Z, so we force it here in case the camera won't move at all) ?
We can notice that the center of the center has kept the original size of subdivisions, but all the quads located in the first 4 subdivisions from the terrain edges are now bigger.
Actually their LOD factor is increased by 1 either on the X axis, either on the Z axis, either on both axes, depending on their location on the global terrain grid.
When it's increased by 1 on both their axes (in the grid corners), their LOD value is 2. This means one of this terrain quad fits exactly 2 x 2 map quads, since the central terrain quads keep fitting each 1 map quad only.
When we move the camera closer to the ground and orientate it to look at some distant hills, we can see that the distant quads are bigger, so depict a larger area of the map (so less detailed) than the close ones.
Let's add now another limit :
terrain.LODLimits = [2, 4];
Playground Example LOD Limits [2, 4] -
Same principle but with an extra step :
The quads in the first 4 subdivisions have all their LOD increased by 1.
The quads in the first 2 subdivisions have their LOD increased again by 1 from their current value, so increased by 2 relatively to the central ones.
We can set as many limits as we want :
terrain.LODLimits = [1, 2, 4];
Playground Example LOD Limits [1, 2, 4] -
We can even repeat a limit as many times we want. In this case, the LOD is incremented as many times as this limit is repeated :
terrain.LODLimits = [1, 1, 1, 1, 2, 4];
Playground Example LOD Limits [1, 1, 1, 1, 2, 4] -
Notes :
.LODLimits
at any time, it's taken in account on the next terrain update. So it's not a fixed value. We could imagine to have different LOD behavior depending on the camera position, speed or on the landscape itself.[4, 2]
). So let's remember this when we have to read this property value. lodX
and lodZ
values what we can read from a custom user function if needed (to be seen further). A simple way to remember how this works :
terrain.LODLimits = [4, 2, 1, 1];
// increment the LOD factor under the 4-th,
// then once again under the second,
// then twice again under the first rows and columns
Example with a bigger terrain : let's rotate slowly the camera or let's zoom in/out to see the perimetric LOD in action
Playground Example Perimetric LOD -
Of course, the perimetric LOD and the camera LOD correction can work together
Playground Example Perimetric and Camera LODs -
[]
), the camera LOD correction applies the same way on all the quads, so even on the already increased perimetric quads. The terrain is updated each time the camera crosses a terrain quad either on the X axis, either on the Z axis.
However, we can set a higher tolerance on each axis to update the camera only after more terrain quads crossed over with the properties .subToleranceX
and .subToleranceZ
.
terrain.subToleranceX = 10; // the terrain will be updated only after 10 quads crossed over by the camera on X
terrain.subToleranceZ = 5; // the terrain will be updated only after 5 quads crossed over by the camera on Z
Playground Example Terrain Update -
In this example, the terrain is updated each time the camera flies over 10 quads on the X axis or 5 quads on the Z axis.
As a consequence, the terrain is moved by bunches of 10 or 5 quads each time it's updated.
The computation charge is constant on each terrain update and depends only on the terrain vertex number.
The tolerance can make the computation occur less often.
This may be useful when the camera moves rarely or by little amount in the terrain area (a character walking on the ground, for instance) or simply if we need more CPU to do other things than updating the terrain.
The default values of both these properties are 1 (minimal authorized value).
They can be changed at any time according to our needs.
The Dynamic Terrain provides the ability to update each of its vertex while the whole terrain update.
Let's enable it (disabled by default). It can be enabled/disabled at any time.
terrain.useCustomVertexFunction = true;
This will be called on next terrain updates, not necesseraly each frame.
// passed parameters :
// - the current vertex
// - the i-th and j-th indexes (column, row)
terrain.updateVertex = function(vertex, i, j) {
// reset the vertex color in case it was formerly modified
vertex.color.g = 1.0;
vertex.color.r = 1.0;
vertex.color.b = 1.0;
// change it above a given altitude
if (vertex.position.y > 2.0) {
vertex.color.b = vertex.position.y / 30.0;
vertex.color.r = vertex.color.b;
}
};
In the following examples slowly rotate the camera or zoom in/out
to see the effect
Playground Example Custom -
The accessible vertex properties are :
Another colored example according to the position on the map
Playground Example Color by Position -
Of course, it works also with alpha
This feature is disabled by default because it may have an impact on the CPU.
Indeed, when a terrain is 100x100 quads, it has 10K vertices and this custom function is then called 10K times.
So let's remember to make it as fast as possible and to not allocate any object within it, else the garbage collector will have to work, consuming our precious FPS.
Let's also remember that this custom user function is called only on terrain updates, not necesseraly each frame. There's a way to force the terrain update on every frame that we'll see further.
The Dynamic Terrain is updated automatically according to the camera position and all the LOD or tolerance parameters we've set so far.
Sometimes it's necessery to do something just before or just after the terrain update although we can't predict in the main logic when this update is triggered.
Therefore, the Dynamic Terrain provides two functions that we can over-write and what are called just before and just after the terrain update : beforeUpdate()
and afterUpdate()
// compute the squared maximum distance in the terrain
var maxD2 = 0.0;
terrain.beforeUpdate = function() {
maxD2 = terrain.terrainHalfSizeX * terrain.terrainHalfSizeZ;
};
terrain.afterUpdate = function() {
maxD2 = 0.0;
};
This may be useful to compute some variable values once for all before they are used then by the user custom function updateVertex()
instead of computing them inside it.
Unless specific need, we shouldn't do this.
terrain.refreshEveryFrame = true; // false, by default
This can be changed at any time at will.
If we need to know if a set of 2D map coordinates (x, z) is currently inside the terrain, we can use the method contains(x, z)
what returns a boolean.
if (terrain.contains(x, z)) {
// do stuff
}
If we need to know what is the altitude on the map of any point located at the coordinatets (x, z) in the World, even if this point is not one of the point defining the map (not one of the points in the map array), we can use the method getHeightFromMap(x ,z)
.
var y = terrain.getHeightFromMap(x, z); // returns y at (x, z) in the World
This method can also return the value of the terrain normal vector at the coordinates (x, z). This value is set to a Vector3 passed as a reference : getHeightFromMap(x, z, ref)
.
var normal = BABYLON.Vector.Zero();
y = terrain.getHeightFromMap(x, z, normal); // update also normal with the terrain normal at (x, z)
var mesh = terrain.mesh; // the terrain underlying BJS mesh
terrain.isAlwaysVisible = true; // default false : if the terrain is always visible in the camera field of view, this will speed up the terrain mesh selection process, so the global scene speed.
var quadSizeX = terrain.averageSubSizeX; // terrain real quad X size
var quadSizeZ = terrain.averageSubSizeZ; // terrain real quad Z size
var terrainSizeX = terrain.terrainSizeX; // terrain current X size
var terrainSizeZ = terrain.terrainSizeZ; // terrain current X size
var terrainHalfSizeX = terrain.terrainHalfSizeX; // terrain current X half size
var terrainHalfSizeZ = terrain.terrainHalfSizeZ; // terrain current Z half size
var terrainCenter = terrain.centerLocal; // Vector3 position of the terrain center in its local space
var terrainWorldCenter = terrain.centerWorld; // Vector3 position of the terrain center in the World space
var mapPointsX = terrain.mapSubX; // the passed map number of points on width at terrain construction time
var mapPointsZ = terrain.mapSubZ; // the passed map number of points on height at terrain construction time
var camera = terrain.camera; // the camera the terrain is linked to. By default, the scene active camera
A color map can be passed to the terrain at construction time.
This color map is a flat array of successive floats between 0 and 1 of each map point (r, g, b) values.
This array must have the same size than the data array.
Let's get back the very first example of the data array generation and let's populate a color array mapColors
var mapSubX = 1000; // map number of points on the width
var mapSubZ = 800; // map number of points on the height
var seed = 0.3; // set the noise seed
noise.seed(seed); // generate the simplex noise, don't care about this
var mapData = new Float32Array(mapSubX * mapSubZ * 3); // x3 because 3 values per point : x, y, z
var mapColors = new Float32Array(mapSubX * mapSubZ * 3); // x3 because 3 values per point : r, g, b
for (var l = 0; l < mapSubZ; l++) { // loop on height points
for (var w = 0; w < mapSubX; w++) { // loop on width points
var x = (w - mapSubX * 0.5) * 5.0; // distance inter-points = 5 on the width
var z = (l - mapSubZ * 0.5) * 2.0; // distance inter-points = 2 on the width
var y = noise.simplex2(x, z); // altitude
mapData[3 * (l * mapSubX + w)] = x;
mapData[3 * (l * mapSubX + w) + 1] = y;
mapData[3 * (l * mapSubX + w) + 2] = z;
// colors of the map
mapColors[3 * (l * mapSubX + w)] = (0.5 + Math.random() * 0.2); // red
mapColors[3 * (l * mapSubX + w) + 1] = (0.5 + Math.random() * 0.4); // green
mapColors[3 * (l * mapSubX + w) + 2] = (0.5); // blue
}
}
And let's pass this color array to the terrain at construction time with the optional parameter property .mapColors
:
var terrainSub = 100; // 100 terrain subdivisions
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
mapColors: mapColors, // the array of map colors
terrainSub: terrainSub // how many terrain subdivisions wanted
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
Remember to try these examples out by slowly rotating the camera or zoom in or out.
Playground Example Color Map -
Obviously this still works with the user custom function called with updateVertex()
:
Playground Example Color Map and Vertex Update -
If we assign a material and a texture to the terrain mesh, it's by default set to the current terrain size and shifted according to the camera movements.
Playground Example Default UV Map -
Before going further, let's note that the texturing works with both the color map and the user custom function
Playground Example Default UV Map and Color Map -
Like for the colors, we could have a set of UVs relative to the map as a flat array of successive floats between 0 and 1 being the u and v values for each map point.
This array must be sized mapSubX x mapSubZ x 2 (because two floats per map point : u and v) and must be passed to the terrain at construction time with the optional parameter property .mapUVs
var terrainSub = 100; // 100 terrain subdivisions
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
mapUVs: mapUVs, // the array of map UVs
terrainSub: terrainSub // how many terrain subdivisions wanted
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
Example :
Here we populate a data map with no altitude (y = 0) and, in the same time, a UV map as a flat array by simply setting the u and v values in the 2D texture relatively to the (x, z) coordinates of each map point.
var mapData = new Float32Array(mapSubX * mapSubZ * 3); // x3 float values per point : x, y and z
var mapUVs = new Float32Array(mapSubX * mapSubZ * 2); // x2 because 2 values per point : u, v
for (var l = 0; l < mapSubZ; l++) {
for (var w = 0; w < mapSubX; w++) {
var x = (w - mapSubX * 0.5) * 2.0;
var z = (l - mapSubZ * 0.5) * 2.0;
mapData[3 *(l * mapSubX + w)] = x;
mapData[3 * (l * mapSubX + w) + 1] = 0.0;
mapData[3 * (l * mapSubX + w) + 2] = z;
mapUVs[2 * (l * mapSubX + w)] = w / mapSubX; // u
mapUVs[2 * (l * mapSubX + w) + 1] = l / mapSubZ; // v
}
}
Then we pass the populated array mapUVs
to the Dynamic Terrain constructor with the optional parameter property mapUVs
:
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
mapUVs: mapUVs, // array of the map UVs
terrainSub: terrainSub // how many terrain subdivisions wanted
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
Playground Example Custom UVmap -
A FreeCamera was set instead of an ArcRotate one to move easily on the map. The map texture is also changed to the file earth.jpg.
As we can notice now, the texture is no longer bound to the terrain itself but to the map : the image is stretched in this example along the whole map.
In this former example, we stretched the image along the whole map.
For this very specific need, we can also the method .createUVMap()
what does the same (computation and assignement to the terrain) in a single call.
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
terrainSub: terrainSub // how many terrain subdivisions wanted
// no more for mapUVs, it will be done by createUVMap()
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
terrain.createUVMap();
// computes and sets an UV map stretching the texture on the whole image
Example with no more manual UV computation
Playground Example UVmap Auto Adjust -
By default, when we assign a data map to the terrain at construction time, it pre-computes all the normals of the map once.
Computing all the map normals is a heavy process, but it's done only once.
This permits to skip the terrain mesh normal recomputation each time this one is morphed, it is to say on each update. Thus, the terrain normal recomputation is disabled by default.
This computation charge would be directly related to the terrain number of vertices (10K for a 100x100 terrain).
var terrainSub = 100; // 100 terrain subdivisions
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
terrainSub: terrainSub // how many terrain subdivisions wanted
// nothing more to do : the map normals are computed at creation time !
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
If for some reason (example : dynamic morphing if the terrain, this will be explained in the part "Without data map"), we need to force the normal computation each update :
terrain.computeNormals = true; // default false, to skip the normal computation
As the normals of map are pre-computed automatically for us, we don't need to care about them.
These normals are stored internally in a flat array of floats, just like the map coordinates.
There is still a way to use a custom normal array if needed.
This flat array of successive floats as normal vector coordinates (x, y, z) for each map point can then be passed to the terrain. It simply must be exactly the same size than the map data array.
This array is passed with the optional parameter property .mapNormals
.
var normalArray = [n1.x, n1.y, n1.z, n2.x, n2.y, n2.z, ...];
var terrainSub = 100; // 100 terrain subdivisions
var params = {
mapData: mapData, // data map declaration : what data to use ?
mapSubX: mapSubX, // how are these data stored by rows and columns
mapSubZ: mapSubZ,
mapNormals: normalArray, // the array of map normals
terrainSub: terrainSub // how many terrain subdivisions wanted
}
var terrain = new BABYLON.DynamicTerrain("t", params, scene);
Example :
This terrain is 300x300 so 90K vertices what is really a huge mesh to compute every update.
Playground Example With a Normal Map, so with automatic pre-computed normals -
Without (computeNormals = true
),
Playground Example Normal Computation each Update -
Check the FPS difference when rotating the camera to feel the gain.
If we have several data sets that we intend to use as data maps, we can precompute all these data set normals with the static method ComputeNormalsFromMapToRef(map, subX, subY, array)
.
var map1 = someFloat32Array;
var map2 = someOtherFloat32Array;
var map3 = someOtherFloat32Array;
var normal1 = new Float32Array(map1.length);
var normal2 = new Float32Array(map2.length);
var normal3 = new Float32Array(map3.length);
// let's precompute the normals of all the maps
BABYLON.DynamicTerrain.ComputeNormalsFromMapToRef(map1, subX1, subY1, normal1);
BABYLON.DynamicTerrain.ComputeNormalsFromMapToRef(map2, subX2, subY2, normal2);
BABYLON.DynamicTerrain.ComputeNormalsFromMapToRef(map3, subX3, subY3, normal3);
A height map is an image file, usually with grey colors only (from black to white), where each pixel color holds the point altitude : the brighter, the higher.
Example file : https://www.babylonjs.com/assets/heightMap.png
Like the BJS MeshBuilder
class provides a method to create a mesh from a height map, the Dynamic Terrain provides a static method to generate a data map from a height map.
Here's the way to use it :
// Declare a callback function that will be executed once the heightmap file is downloaded
// This function is passed the generated data and the number of points on the map height and width
var terrain;
var createTerrain = function(mapData, mapSubX, mapSubZ) {
var options = {
terrainSub: 100, // 100 x 100 quads
mapData: mapData, // the generated data received
mapSubX: mapSubX, mapSubZ: mapSubZ // the map number of points per dimension
};
terrain = new BABYLON.DynamicTerrain("dt", options, scene);
terrain.createUVMap(); // compute also the UVs
terrain.mesh.material = someMaterial;
// etc about the terrain ...
// terrain.updateCameraLOD = function(camera) { ... }
};
// Create the map from the height map and call the callback function when done
var hmURL = "https://www.babylonjs.com/assets/heightMap.png"; // heightmap file URL
var hmOptions = {
width: 5000, height: 4000, // map size in the World
subX: 1000, subZ: 800, // number of points on map width and height
onReady: createTerrain // callback function declaration
};
var mapData = new Float32Array(1000 * 800 * 3); // the array that will store the generated data
BABYLON.DynamicTerrain.CreateMapFromHeightMapToRef(hmURL, hmOptions, mapData, scene);
hmURL
is a string, it's the URL or the DataURL string of the height map image,width
and height
are optional floats (default 300), the dimensions the map in the World, subX
and subZ
are optional integers (default 100), the number of points on each map dimension,minHeight
and maxHeight
are the optional minimal and maximal heights (floats, default 0 and 10), offsetX
and offsetZ
are optional floats (default 0) to shift the map, what is centered around the World origin by default, along the X or Z World axes, onReady
is an optional callback function to be called when the data are generated. It's passed the data array and the number of points per map dimension, mapData
is a float array, sized subX x subZ x 3, scene
is the scene that will store the downloaded image in its internal database. Let's note that, if we need to create the terrain in the callback function, it's not needed to use this kind of function to precompute in advance some data set from different images for further use :
var url1 = someURL;
var url2 = someOtherURL;
var url3 = someOtherURL;
// all my maps will have the same subdivisions and dimensions
// no callback function here
var options = {width: 5000, height: 4000, subX: 1000, subZ: 800};
var set1 = new Float32Array(subX * subZ * 3);
var set2 = new Float32Array(subX * subZ * 3);
var set3 = new Float32Array(subX * subZ * 3);
BABYLON.DynamicTerrain.CreateMapFromHeightMapToRef(url1, options, set1, scene);
BABYLON.DynamicTerrain.CreateMapFromHeightMapToRef(url2, options, set2, scene);
BABYLON.DynamicTerrain.CreateMapFromHeightMapToRef(url3, options, set3, scene);
createUVMap()
and the world height map to define the altitudes.
The terrain can be assigned another map at any time.
Example :
// change the terrain map on the fly
if (camera.position.z > someLimit) {
terrain.mapData = map2;
}
This can be useful if new data are dynamically downloaded as the camera moves in the World.
Not only the map data can be changed, but also the color, uv or normal data.
// change the terrain map on the fly
if (camera.position.z > someLimit) {
terrain.mapData = map2;
terrain.mapColors = colors2;
terrain.mapUVs = uvs2;
terrain.mapNormals = normals2;
}
Let's note that when we assign a new data map to a terrain, the normal map of this map is not automatically recomputed.
Thus we have two options :
terrain.precomputeNormalsFromMap = true
. In this case, every new data map assignement to the terrain will trigger the map normal computation on the fly, terrain.precomputeNormalsFromMap = true; // default = false
terrain.mapData = map2; // the normal map is automatically computed on the hood
// then, later in the code ... terrain.mapData = map2; terrain.mapNormals = normals2;
### Without Data Map
The Dynamic Terrain is a right tool to render a part of a map of 3D data.
We usually don't need to modify the map data because they are just what we want to render on the screen.
However the Dynamic Terrain is ... _dynamic_.
This means that it can be used for other purposes than just render a 3D map.
For instance, it can be generated without any data map :
```javascript
var terrainSub = 140; // terrain subdivisions
var terrainOptions = { terrainSub: terrainSub };
var terrain = new BABYLON.DynamicTerrain("dt", terrainOptions, scene);
Actually, we could even not pass the terrainSub
and the terrain would still be generated with a size of 60x60.
A Dynamic Terrain generated without any data map looks like a simple planar ribbon initially
Playground Example No Data Map -
Of course we can always add to it some LOD behavior (perimetric or camera LOD) like to any standard terrain created with a data map.
But it may be interesting to use in this case the user custom function and to modify the terrain vertex positions, something we wouldn't probably want to do with a data map generated terrain.
terrain.useCustomVertexFunction = true;
terrain.updateVertex = function(vertex, i, j) {
vertex.position.y = 2.0 * Math.sin(i / 5.0) * Math.cos(j / 5.0);
};
Playground Example No Data Map but Vertex Update -
Let's remember that, when enabled, the method updateVertex
is called only on each terrain update (so when the camera moves), not necesserily every frame.
If we need to give the terrain an extra animation, we can set its property .refreshEveryFrame
to true and add, for instance, a movement depending on the time :
var t = 0.0;
terrain.useCustomVertexFunction = true;
terrain.refreshEveryFrame = true;
terrain.computeNormals = true;
// user custom function : now the altitude depends on t too.
terrain.updateVertex = function(vertex, i, j) {
vertex.position.y = 2.0 * Math.sin((vertex.position.x + t) / 5.0) * Math.cos((vertex.position.z + t) / 5.0);
};
// scene animation
scene.registerBeforeRender(function() {
t += 0.01;
});
Playground Example Refresh Every Frame -
The CPU load required by the method updateVertex()
is depending of course on what it does, but also on the terrain number of vertices.
Let's note that, as we computationally change each terrain vertex altitude, the normal computation must be forced (terrain.computeNormals = true
) to get a right light reflection with plain triangles:
Playground Example Forced Normal Computation -
Important note :
We used here the parameters i
, j
and the vertex position
property.
i
is the vertex index on the terrain X axis, it's an integer valued between 0 and terrainSub
, both included.j
is the vertex index on the terrain Z axis, it's an integer valued between 0 and terrainSub
, both included.x
and z
are always positive, this means the terrain is NOT centered in its local space but starts from the system origin at i = 0 and j = 0 : the first terrain vertex is at (0, 0) in the plane (xOz), the other vertices have then positive x and z coordinate values only.