Yesterday I wrote about automatically blending textures together based on terrain height. Tonight’s topic is how do we give a terrain its height, and more specifically how can we let the user do so.
First off we need to setup some config variables and create the land geometry (again, I am using the three.js WebGL framework).
/** CONFIG **/
var plots_x = 40; // How many land spaces there are along the X axis
var plots_y = 40; // How many land spaces there are along the Y axis
var plot_vertices = 2; // How many vertices each land space has along each axis
// Defines the x/y coordinates of the map's top-left corner.
var map_left = plots_x / -2;
var map_top = plots_y / -2;
// Create the land
var ground = new THREE.Mesh(
new THREE.PlaneGeometry( plots_x, plots_y, plots_x * plot_vertices, plots_y * plot_vertices ),
new THREE.MeshShaderMaterial({
uniforms: {
texture_grass: { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture( 'texture_ground_grass.jpg' ) },
texture_bare: { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture( 'texture_ground_bare.jpg' ) },
texture_snow: { type: "t", value: 2, texture: THREE.ImageUtils.loadTexture( 'texture_ground_snow.jpg' ) },
show_ring: { type: 'i', value: true },
ring_width: { type: 'f', value: 0.15 },
ring_color: { type: 'v4', value: new THREE.Vector4(1.0, 0.0, 0.0, 1.0) },
ring_center: { type: 'v3', value: new THREE.Vector3() },
ring_radius: { type: 'f', value: 5.0 }
},
attributes: {
displacement: { type: 'f', value: [] }
},
vertexShader: document.getElementById( 'groundVertexShader' ).textContent,
fragmentShader: document.getElementById( 'groundFragmentShader' ).textContent
})
);
ground.dynamic = true;
ground.displacement = ground.materials[0].attributes.displacement;
for (var i = 0; i < ground.geometry.vertices.length; i++) {
ground.materials[0].attributes.displacement.value.push(0);
}
ground.rotation.x = Degrees2Radians(-90);
scene.addChild(ground);
This creates the plane we will use as the ground. Also defined are some uniform and attribute values which will be passed to the WebGL shaders during rendering. The `texture_*` uniforms hold the land textures and the `*ring*` uniforms specify how the red ring around the user’s cursor will look (see screenshot above). Also defined is the `displacement` attribute, which is then filled with zeroes – one zero for each vertex of the ground object. This attribute tells the shader about the terrain’s elevation at different points allowing the shader to move the vertices around.
You may be wondering why I’m not using a heightmap image here. The first reason is because at the moment, using textures in a vertex shader isn’t supported by many browser implementations – the only browser I know supports it is Chrome Canary. Secondly, storing the elevation information in an array is more efficient because the user can modify the terrain themselves. As you’ll see below, using the array method we can change the individual vertex points, if a heightmap image was used we’d have to implement an entire drawing interface to modify that heightmap.
The next part of our terrain program defines a couple helper functions we will need to determine which vertices are affected by the user’s actions. There are two of these functions. First there is `verticeIndex` and takes a vertex’s coordinates and returns the index pointing to it in our displacement array. The second is `findLattices` which, using a point origin, returns all vertices in a radius. I don’t want to clutter this post up with these functions so I have put them in this text file if you want to take a look at them.
Now for the user interaction part. When the user clicks on the map and drags their mouse the terrain needs to move up or down depending on the tool they’ve selected.
this.onmousemove = function() {
if (mouse_info.state === 2) { // The user has clicked and drug their mouse
// Get all of the vertices in a 5-unit radius
var vertices = findLattices(5 * plot_vertices, mouse_info.vertex_coordinates);
// Call the landscaping tool to do its job
this.tools[landscape_tool](5 * plot_vertices, vertices);
// Ensure all of the vertices are within the elevation bounds
var vertice_index;
for (var i = 0; i < vertices.length; i++) {
vertice_index = verticeIndex(vertices[i]);
if (ground.displacement.value[vertice_index] > 8) {
ground.displacement.value[vertice_index] = 8;
}
if (ground.displacement.value[vertice_index] < -7) {
ground.displacement.value[vertice_index] = -7;
}
ground.water.displacement.value[vertice_index] = ground.displacement.value[vertice_index];
}
ground.water.displacement.needsUpdate = true;
}
};
In the code above, this.tools[landscape_tool](5 * plot_vertices, vertices); makes a call to this function:
function(radius, vertices) {
var i, vertice, vertice_index, distance;
for (i = 0; i < vertices.length; i++) {
vertice = vertices[i];
if (vertice.x < 0 || vertice.y < 0) { // Sanity check
continue;
}
if (vertice.x >= plots_x * plot_vertices + 1 || vertice.y >= plots_y * plot_vertices + 1) { // Sanity check
continue;
}
vertice_index = verticeIndex(vertice); // Turn the vertice's x/y coordinates into its array index
distance = Math.sqrt(Math.pow(mouse_info.vertex_coordinates.x - vertice.x, 2) + Math.pow(mouse_info.vertex_coordinates.y - vertice.y, 2)); // Calculate the vertice's distance from where the cusor is
ground.displacement.value[vertice_index] += Math.pow(radius - distance, .5) * .03; // Apply the new displacement value
ground.displacement.needsUpdate = true; // Flag this attribute for update (needed by three.js)
}
}
The above function simply finds all of the vertices in a radius, calculates how far they are from the user’s cursor, and changes the terrain’s displacement based on that distance. Putting all of these functions together yields a small application where users can create their own mountains and valleys – throw in a second plane for water and they can even have lakes and rivers!
To view all of this code in action visit its demo page.
Follow me on Twitter
Follow me on GitHub
I want to use your example as a jumpoff — however I want to load height data based on my game world. How can I coordinate height data that I have already with your verticle / texture map? (for ex., I have a 512 x 512 grid with values from -10000 to 10000
Dave, I emailed you a response but I don’t know if you got it. Here it is again:
If I understand correctly, you have a 512×512 grid of height values that you want to apply to a terrain. Following the example I posted, you would apply your heightmap data at these lines:
for (var i = 0; i < ground.geometry.vertices.length; i++) {
ground.materials[0].attributes.displacement.value.push(0);
}
The push( 0 ) is defaulting all elevation points to 0. You can replace the zero with the array values from your existing game data. You'll also have to create the terrain to be 512×512 vertices to match it up with your dataset:
new THREE.PlaneGeometry( terrain_width, terrain_length, 511, 511 ), // pass 511 because that determines the number of faces, vertex count = faces + 1
Hope that helps, if you have any more questions feel free to ask!
Pingback: WebGL around the net, 11 August 2011 | Learning WebGL
Pingback: Internette 3 Boyutlu Nesneler – WEBGL | GÜNEŞİN TAM İÇİNDE - Sarışın Site
Pingback: Dynamic terrain without heightmaps | Farkas Máté