I have dug myself into developing another WebGL game and so far it’s been going very well. It’s a city builder in the vein of SimCity and so far I have a number of the foundations set pretty well. One of the biggest challenges so far has been the terrain editing. While I’ll have a post tomorrow dedicated to displacements and editing heightmaps in WebGL, tonight I’ll stick to a simple topic: blending textures on the terrain. The goal is to have textures which will automatically blend themselves together depending on the geography’s elevation. This means having a base texture and defining secondary textures at different height levels. For WebGL I’m using the three.js framework so any Javascript code here will be based on that library.
First we need to create a geometry plane to act as our terrain and give it a shader material:
var ground = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
new THREE.MeshShaderMaterial({
uniforms: {
texture_grass: { type: "t", value: 0, texture: THREE.ImageUtils.loadTexture('grass.jpg') },
texture_rock: { type: "t", value: 1, texture: THREE.ImageUtils.loadTexture('rock.jpg') },
},
vertexShader: document.getElementById( 'groundVertexShader' ).textContent,
fragmentShader: document.getElementById( 'groundFragmentShader' ).textContent
})
);
This assumes you have two image files, grass.jpg and rock.jpg, in the same folder as your HTML file. It also assumes you have two <script> blocks defined with IDs of `groundVertexShader` and `groundFragmentShader`. These script blocks are used to hold the GLSL shader code that sets the colors used at each point on the geometric plane. Here is the shader code I’m using:
<script id="groundVertexShader" type="x-shader/x-fragment">
varying vec2 vUv;
varying vec3 vPosition;
void main( void ) {
vUv = uv;
vPosition = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1);
}
</script>
<script id="groundFragmentShader" type="x-shader/x-vertex">
uniform sampler2D texture_grass;
uniform sampler2D texture_rock;
varying vec2 vUv;
varying vec3 vPosition;
void main() {
// Texture loading
vec4 diffuseGrass = texture2D( texture_grass, vUv );
vec4 diffuseSand = vec4(.8, .8, .7, 1.0);
vec4 diffuseRock = texture2D( texture_rock, vUv );
vec4 diffuseSnow = vec4(.8, .9, 1.0, 1.0);
vec4 color = diffuseGrass; // grass base
// add sand
color = mix(
diffuseSand,
color,
min(abs(.0 - vPosition.z) / .03, 1.0) // Start at .0 for .03 units
);
// add rock
color = mix(
diffuseRock,
color,
min(abs(.6 - vPosition.z) / .6, 1.0) // Start at .6 for .6 units
);
// add snow
color = mix(
diffuseSnow,
color,
min(abs(1.3 - vPosition.z) / .5, 1.0) // Start at 1.3 for .5 units
);
gl_FragColor = color;
}
</script>
In the top script block is the vertex shader. It’s job is to move any of the geometry’s vertices around. In this case we just set some variables for the fragment shader to use and move on – no changes needed. The bottom script block contains the fragment shader. It begins with import declarations (the `uniform`s and `varying`s) – these declare the two textures of the same names provided by the Javascript and also the two variables set by the vertex shader.
The first four lines of the main() function define the colors we could be using for these fragment. Both the grass and rock textures are converted to their colors based on the vertice’s UV values while the sand and snow colors are hardcoded, not changing based on what point of the terrain they are at.
The rest of the function determines how much of each texture is used for that fragment’s color. The grass texture is assigned to the `color` variable as the base. Next, sand is added based on how far away the vertex is from where sand is supposed to be: sand’s location starts at 0.0 elevation and extends .03 units both up and down, fading out as it moves away from 0.0. The shader compares against the vertex’s Z position because planes are created with a width (x) and height (y), no depth (z). This means changes that we perceive as terrain height actually happen on the Z axis.
Rock and snow textures and calculated in the same way. Finally, the `color` variable is assigned to `gl_FragColor` which is then exported as the fragment’s color. This yields blended textures which transition smoothly based on the height of the terrain.
To tie it together, throw in some elevation data and you’ve got yourself an auto-texturing piece of land.
Follow me on Twitter
Follow me on GitHub
Pingback: WebGL around the net, 11 August 2011 | Learning WebGL
Hi:
I’m trying to work out this tutorial. I have the files three.js, map.js, heighmap.html and two textures grass.jpg and rock.jpg of 600×400 px the first image and 300×225 the second but it doesn’t work for me. In firefox I get the error: Security error h.TEXTURE_MAG_FILTER,W……. on Three.js line 293. Can you help me please?
Do you have your code up online anywhere to look at? I don’t recognize that error. Also, WebGL requires texture dimensions to be powers of 2: 128, 256, 512, etc.
Now I resize the two images to 256×256 px but I get the same error. I’m trying to work out this example locally but if I don’t get some results I’ll up the code.
Thanks for your answer!
Developing WebGL locally opens up a couple can of worms. Reading through this should help: https://github.com/mrdoob/three.js/wiki/How-to-run-things-locally
Finally it works! the problem was the parameter security.fileuri.strict_origin_policy true (in firefox). I change it and your example run locally. Thank you for the help!
Hi there, i was trying to do something like that.
but i want to load data (height and textures) from a file or an array.
How can I do that?
Like, i know how to put the height with three.js, in each vertices, but i dont know how to define a textures for each vertices.
Pingback: PeaksAndValleys Added to WebGL-Samples Repository «BlackBerry Developer Blog
Pingback: PeaksAndValleys Added to WebGL-Samples Repository | BlackBerry and Phone Shop