Follow me on Twitter Follow me on GitHub
Chandler Prall Thoughts & Experiments for the Web

Dynamic terrain without heightmaps

View live demo.
Terrain Height

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.

7 Responses to Dynamic terrain without heightmaps

  1. 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

    • chandler says:

      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!

  2. Pingback: WebGL around the net, 11 August 2011 | Learning WebGL

  3. Pingback: Internette 3 Boyutlu Nesneler – WEBGL | GÜNEŞİN TAM İÇİNDE - Sarışın Site

  4. Pingback: Dynamic terrain without heightmaps | Farkas Máté

  5. DC Nelson says:

    I’m trying to migrate this over to three.js R58 (it’s currently on R41) and i’m encountering problems with all the changes that have been made.

    I understand that MeshShaderMaterial is now just ShaderMaterial, and that the uniforms auto set (what was) their own values, so:

    new THREE.MeshShaderMaterial({

    uniforms: {

    texture_grass: { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture( 'texture_ground_grass.jpg' ) },

    becomes:

    new THREE.ShaderMaterial({

    uniforms: {

    texture_grass: { type: "t", value: THREE.ImageUtils.loadTexture( 'texture_ground_grass.jpg' ) },

    or alteast i think it does. But this then leads to an error regarding the materials and attributes.

    ground.displacement = ground.materials[0].attributes.displacement;

    Any help would be greatly appreciated.

    Regards

  6. Pingback: webgl three.js get mouse position on Mesh | BlogoSfera

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>