# How to create a multiple textured terrain

I am looking for a way to create a terrain textured with multiple tiles, blended by a blend map.

What you are looking for is a technique called texture splatting. There is no texture splatting solution available in the official `three.js` repository. But there are some existing solutions in the web like this one:

You should also have a look at the following topic: Basic Texture Splatting, need help

I am studying the code from Lee Stemkoski, the shader is not very complicated but it uses terrain height if Iām correct. I need to change it to use a blend map. My idea is that the blend map should work as follows:

• There is a basic ground texture
• four other textures should be added based on the R, G, B and A value.

Not sure yet how to get there, but I have changed my mindset from āNeed to sort this stupid problem out fastā to āThatās a nice experiment!ā - looking forward to the journey and will keep you posted. Please feel free to weigh in if you got ideasā¦

2 Likes

Got it working a rudimentary form:

``````uniform sampler2D blendTexture;
uniform sampler2D baseTexture;
uniform sampler2D sandyTexture;
uniform sampler2D grassTexture;
uniform sampler2D rockyTexture;
uniform sampler2D snowyTexture;

varying vec2 vUV;

varying float vAmount;

void main()
{

vec4 blend=texture2D( blendTexture, vUV );
float baseWeight=1.0 - max(blend.r, max(blend.g, blend.b));

vec4 base =  baseWeight * texture2D( baseTexture, vUV * 10.0 );
vec4 sandy = blend.r * texture2D( sandyTexture, vUV * 10.0 );
vec4 grass = blend.g * texture2D( grassTexture, vUV * 10.0 );
vec4 rocky = blend.b * texture2D( snowyTexture, vUV * 20.0 );
gl_FragColor = (vec4(0.0, 0.0, 0.0, 1.0) + base + sandy + grass + rocky) / (baseWeight+blend.r+blend.g+blend.b);

}
``````

5 Likes

Hint: When you post code in this forum, please use backticks in order to open and close a code section. This will improve formatting and readability. Iāve edited your post so you can see how itās done.

1 Like

Iāve actually been looking for a texture splatting example that can use alpha pixels. One where I can actually use RGBA pixels & draw āonā the texture. āNotā the example stemkoski created.

I am now trying to implement lighting and bumpmapping. I would guess that all we need is the exact same process the built in shaders use after the final texture source color has been determined.

Does anyone know where I can find the code for the default shaders? Like the Phong shaders? Or some documentation on how to implement ShaderChunks might also come in handy.

Itās a bit silly to keep replying to my own post, but I think this may be helpful to others.

I now have a demo with working lighting and normal mapping. I took a different approach to get here. Instead of creating a custom material with my own shader, I now use onBeforeCompile to replace some shader chunks to suit my needs, leaving all others intact. This allows me to start with any material.

I then replace the mapping chunks and the normalmapping chunks for the fragment shader - leaving the vertex shader as it is.

The result is seen here: Terrain tiles blending demo #4

Navigate using W (forward), S (backward), A (turn left) and D (turn right) keys.

`

``````<script id="tileblend_pars_fragment" type="x-shader/x-vertex">
#define USE_MAP
uniform sampler2D texBlendMap;
uniform sampler2D texBaseColor;
uniform sampler2D texTile1Color;
uniform sampler2D texTile2Color;
uniform sampler2D texTile3Color;
//uniform sampler2D texTile4Color;

uniform float repeatBase;
uniform float repeatTile1;
uniform float repeatTile2;
uniform float repeatTile3;
//uniform float repeatTile4;

</script>

vec4 tbBlend=texture2D( texBlendMap, vUv );
float tbBaseWeight=1.0 - max(tbBlend.r, max(tbBlend.g, tbBlend.b));

vec4 base =  tbBaseWeight * texture2D( texBaseColor, vUv * repeatBase );
vec4 color1 = tbBlend.r * texture2D( texTile1Color, vUv * repeatTile1 );
vec4 color2 = tbBlend.g * texture2D( texTile2Color, vUv * repeatTile2 );
vec4 color3 = tbBlend.b * texture2D( texTile3Color, vUv * repeatTile3 );
vec4 newColor = (vec4(0.0, 0.0, 0.0, 1.0) + base + color1 + color2 + color3) / (tbBaseWeight+tbBlend.r+tbBlend.g+tbBlend.b);

diffuseColor = newColor;

</script>

#ifdef USE_NORMALMAP
uniform sampler2D normalMap;
uniform vec2 normalScale;

uniform sampler2D texBaseBump;
uniform sampler2D texTile1Bump;
uniform sampler2D texTile2Bump;
uniform sampler2D texTile3Bump;
uniform sampler2D texTile4Bump;

#ifdef OBJECTSPACE_NORMALMAP
uniform mat3 normalMatrix;
#else
// Per-Pixel Tangent Space Normal Mapping
// http://hacksoflife.blogspot.ch/2009/11/per-pixel-tangent-space-normal-mapping.html
vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm ) {
// Workaround for Adreno 3XX dFd*( vec3 ) bug. See #9988
vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );
vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );
vec2 st0 = dFdx( vUv.st );
vec2 st1 = dFdy( vUv.st );
float scale = sign( st1.t * st0.s - st0.t * st1.s ); // we do not care about the magnitude
vec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );
vec3 T = normalize( ( - q0 * st1.s + q1 * st0.s ) * scale );
vec3 N = normalize( surf_norm );
mat3 tsn = mat3( S, T, N );

vec4 tbBlend=texture2D( texBlendMap, vUv );
float tbBaseWeight=1.0 - max(tbBlend.r, max(tbBlend.g, tbBlend.b));

float foundIdx=0.0;
float foundVal=tbBaseWeight;

if (tbBlend.r>foundVal) { foundIdx=1.0; foundVal=tbBlend.r;}
if (tbBlend.g>foundVal) { foundIdx=2.0; foundVal=tbBlend.g;}
if (tbBlend.b>foundVal) { foundIdx=3.0; foundVal=tbBlend.b;}

vec3 mapN = texture2D( texBaseBump, vUv * repeatBase ).xyz * 2.0 - 1.0;
if (foundIdx==1.0) mapN = texture2D( texTile1Bump, vUv * repeatTile1 ).xyz * 2.0 - 1.0;
else if (foundIdx==2.0) mapN = texture2D( texTile2Bump, vUv * repeatTile2 ).xyz * 3.0 - 1.0;
else if (foundIdx==3.0) mapN = texture2D( texTile3Bump, vUv * repeatTile3 ).xyz * 3.0 - 1.0;

mapN.xy *= normalScale;
mapN.xy *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );
return normalize( tsn * mapN );
}
#endif
#endif
``````

`

And the onBeforeCompile bit:

``````// --------------------------------------------------------
// Customizing Material
terrainMaterial.userData.myValue = { type:"f",value: 2.0 } //this will be our input, the system will just reference it

shader.uniforms.myValue = terrainMaterial.userData.myValue //pass this input by reference
shader.uniforms.texBlendMap=	{ type: "t", value: blendTexture };

shader.uniforms.texBaseColor=	{ type: "t", value: baseTexture };
shader.uniforms.texBaseBump =	{ type: "t", value: baseBump };
shader.uniforms.repeatBase=		{ type: "f", value: 50};

shader.uniforms.texTile1Color=	{ type: "t", value: grassTexture };
shader.uniforms.texTile1Bump =	{ type: "t", value: grassBump };
shader.uniforms.repeatTile1=	{ type: "f", value: 20};

shader.uniforms.texTile2Color=	{ type: "t", value: snowyTexture };
shader.uniforms.texTile2Bump =	{ type: "t", value: snowyBump };
shader.uniforms.repeatTile2=	{ type: "f", value: 35};

shader.uniforms.texTile3Color=	{ type: "t", value: rockyTexture };
shader.uniforms.texTile3Bump =	{ type: "t", value: rockyBump };
shader.uniforms.repeatTile3=	{ type: "f", value: 19};

//shader.uniforms.texTile4Color=	{ type: "t", value: sandyTexture };
//shader.uniforms.texTile4Bump =	{ type: "t", value: sandyBump };
//shader.uniforms.repeatTile4=	{ type: "f", value: 20};

'#include <map_fragment>',
document.getElementById( 'tileblend_fragment'   ).textContent
)
'#include <normalmap_pars_fragment>',
document.getElementById( 'normalmap_pars_fragment'   ).textContent
)
}
``````

`

I am not happy with the shadows, but those are weird even when the material is not modified, so Iāll start a different topic for it.

2 Likes

@Fedor_van_Eldijk : Actually Iām glad you keep replying! Itās helping me āalotā & I really appreciate all that you do & are doing! Can you add slope angle collision? I just wanna use a splatmap with alpha to draw on the Terrain.

2 Likes

I am not sure if I need that for my purpose, this demo is just a splatmap. It uses a base tile and can handle four different splatting textures using the R, G, B and A channels.

In my project, the splatmap will be generated offline by a map generating tool. I might add a painting function to it later, but for now it is all I need. However you feel free to rip the code and extend it.

@Fedor_van_Eldijk : Do you have an example of using splat maps created from a program like /Paint.net/ & a way to make the texture look better on the shader by decreasing the scaled textureās size?

Thank you very much!

Thank you so much for doing this. Been looking for something like this forever!!!

Gave me a very good place to branch of.

Thanks x a billion!!!

I use this technique in my game also, found the stackoverflow question a couple of years back.
Hereās the result:

Iāve had a bit of an issue packing all 4 splat masks into a single PNG, didnāt find good tools for that. Instead, I load 4 separate grayscale masks and combine them into a single DataTexture inside the engine.

sand:

dirt:

rock:

grass:

I only use diffuse textures. Based on some experiements, i came to a conclusion that even this already strains number of texture units, adding more makes your shader unusable on some of the lower-end mobile devices.

3 Likes

Looks great!

Yea I ran into a texture unit issue when I added normal and specular mapping (with shadows). Had to workaround by passing blendmap into the normal map perturbnormal function instead of re-fetching it. Also instead of using the āBaseā diffuse,normal and specular maps as uniforms I just use those as the material map, normalMap and specularMaps.

Just for the heck of it hereās the shader code:

``````	// SPLAT
terrainSplatTexture: {

tileblend_pars_fragment: [

'#define USE_MAP',
'uniform sampler2D texBlendMap;',
//'uniform sampler2D texBaseColor;',
'uniform sampler2D texTile1Color;',
'uniform sampler2D texTile2Color;',
'uniform sampler2D texTile3Color;',

'uniform float repeatBase;',
'uniform float repeatTile1;',
'uniform float repeatTile2;',
'uniform float repeatTile3;',

'uniform int tileCount;'

].join('\n'),

tileblend_fragment: [

'vec4 tbBlend;',

'float tbBaseWeight;',

'if ( tileCount > 1 ){',

'tbBlend = texture2D( texBlendMap, vUv );',

'tbBaseWeight = 1.0 - max(tbBlend.r, max(tbBlend.g, tbBlend.b));',

'vec4 texelColor = vec4(0.0, 0.0, 0.0, 1.0);',

'texelColor += tbBaseWeight * texture2D( map, vUv * repeatBase );',

'texelColor += tbBlend.r * texture2D( texTile1Color, vUv * repeatTile1 );',

'tbBaseWeight += tbBlend.r;',

'if( tileCount > 2 ) {',

'texelColor += tbBlend.g * texture2D( texTile2Color, vUv * repeatTile2 );',

'tbBaseWeight += tbBlend.g;',

'}',

'if( tileCount > 3 ) {',

'texelColor += tbBlend.b * texture2D( texTile3Color, vUv * repeatTile3 );',

'tbBaseWeight += tbBlend.b;',

'}',

'texelColor = texelColor / tbBaseWeight;',

'texelColor.a = tbBlend.a;',

//'diffuseColor = mapTexelToLinear( texelColor );',
'diffuseColor = texelColor;',

'} else {',

'vec4 texelColor = texture2D( map, vUv );',

'texelColor = mapTexelToLinear( texelColor );',

'diffuseColor *= texelColor;',

'}',

].join('\n'),

normalmap_pars_fragment: [

'#ifdef USE_NORMALMAP',
'uniform sampler2D normalMap;',
'uniform vec2 normalScale;',

//'uniform sampler2D texBaseBump;',
'uniform sampler2D texTile1Bump;',
'uniform sampler2D texTile2Bump;',
'uniform sampler2D texTile3Bump;',

'#ifdef OBJECTSPACE_NORMALMAP',
'uniform mat3 normalMatrix;',
'#else',

'// Per-Pixel Tangent Space Normal Mapping',

'// http://hacksoflife.blogspot.ch/2009/11/per-pixel-tangent-space-normal-mapping.html',

'vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec4 tbBlend ) {',

'// Workaround for Adreno 3XX dFd*( vec3 ) bug. See #9988',
'vec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );',
'vec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );',
'vec2 st0 = dFdx( vUv.st );',
'vec2 st1 = dFdy( vUv.st );',
'float scale = sign( st1.t * st0.s - st0.t * st1.s ); // we do not care about the magnitude',
'vec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );',
'vec3 T = normalize( ( - q0 * st1.s + q1 * st0.s ) * scale );',
'vec3 N = normalize( surf_norm );',
'mat3 tsn = mat3( S, T, N );',

'vec3 mapN;',

'if( tileCount > 1 ){',

//'vec4 tbBlend=texture2D( texBlendMap, vUv );',
'float tbBaseWeight=1.0 - max(tbBlend.r, max(tbBlend.g, tbBlend.b));',

'float foundIdx=0.0;',
'float foundVal=tbBaseWeight;',

'if (tbBlend.r>foundVal) { foundIdx=1.0; foundVal=tbBlend.r;}',
'if (tbBlend.g>foundVal) { foundIdx=2.0; foundVal=tbBlend.g;}',
'if (tbBlend.b>foundVal) { foundIdx=3.0; foundVal=tbBlend.b;}',

'mapN = texture2D( normalMap, vUv * repeatBase ).xyz * 2.0 - 1.0;',

'if (foundIdx==1.0) mapN = texture2D( texTile1Bump, vUv * repeatTile1 ).xyz * 2.0 - 1.0;',

'else if (foundIdx==2.0 && tileCount > 2) mapN = texture2D( texTile2Bump, vUv * repeatTile2 ).xyz * 2.0 - 1.0;',

'else if (foundIdx==3.0 && tileCount > 3) mapN = texture2D( texTile3Bump, vUv * repeatTile3 ).xyz * 2.0 - 1.0;',

'} else {',

'mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;',

'}',

'mapN.xy *= normalScale;',

'mapN.xy *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );',

'return normalize( tsn * mapN );',

'}',
'#endif',
'#endif'

].join('\n'),

normalmap_fragment_maps: [

'#ifdef USE_NORMALMAP',
'#ifdef OBJECTSPACE_NORMALMAP',
'normal = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;',
'#ifdef FLIP_SIDED',
'normal = - normal;',
'#endif',
'#ifdef DOUBLE_SIDED',
'normal = normal * ( float( gl_FrontFacing ) * 2.0 - 1.0 );',
'#endif',
'normal = normalize( normalMatrix * normal );',
'#else',
'#ifdef USE_TANGENT',
'mat3 vTBN = mat3( tangent, bitangent, normal );',
'vec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;',
'mapN.xy = normalScale * mapN.xy;',
'normal = normalize( vTBN * mapN );',
'#else',
'normal = perturbNormal2Arb( -vViewPosition, normal, tbBlend );',
'#endif',
'#endif',
'#elif defined( USE_BUMPMAP )',
'normal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );',
'#endif',

].join('\n'),

specularmap_pars_fragment: [

'#ifdef USE_SPECULARMAP',

'uniform sampler2D specularMap;',
//'uniform sampler2D texBaseSpecular;',
'uniform sampler2D texTile1Specular;',
'uniform sampler2D texTile2Specular;',
'uniform sampler2D texTile3Specular;',

'#endif'

].join('\n'),

specularmap_fragment: [

'float specularStrength;',

'#ifdef USE_SPECULARMAP',

'vec4 texelSpecular;',

'if( tileCount > 1 ){',

//'vec4 tbBlend=texture2D( texBlendMap, vUv );',
//'float tbBaseWeight=1.0 - max(tbBlend.r, max(tbBlend.g, tbBlend.b));',

'float foundIdx=0.0;',

'float foundVal = tbBaseWeight;',

'if (tbBlend.r > foundVal) { foundIdx=1.0; foundVal=tbBlend.r; }',
'if (tbBlend.g > foundVal) { foundIdx=2.0; foundVal=tbBlend.g; }',
'if (tbBlend.b > foundVal) { foundIdx=3.0; foundVal=tbBlend.b; }',

'vec4 texelSpecular = texture2D( specularMap, vUv * repeatBase );',

'if (foundIdx==1.0) texelSpecular = texture2D( texTile1Specular, vUv * repeatTile1 );',

'else if (foundIdx==2.0) texelSpecular = texture2D( texTile2Specular, vUv * repeatTile2 );',

'else if (foundIdx==3.0) texelSpecular = texture2D( texTile3Specular, vUv * repeatTile3 );',

'}else{',

'texelSpecular = texture2D( specularMap, vUv );',

'}',

'specularStrength = texelSpecular.r;',

'#else',

'specularStrength = 1.0;',

'#endif'

].join('\n')

}``````
1 Like

That is brilliant!! Thank you for posting that! I have been trying to work out a low-impact way to texture a terrain, and I think this would work wonderful. Say with mountaintops, I could have a snowy textureā¦ then a corresponding mask that you just airbrush in where you want those bits.

Can anyone modify this shader to allow for a āsplat mapā for RGBA? Ty VERY much for this EXCELLENT project & keep up the great work!

What exactly do you mean? Is this about using the A-channel as well - because that I did not get arount yet too, so I would second that request.

@Fedor_van_Eldijk Yes, Alpha channel. Just like this example splat map image I have created that contains Red, Green, Blue, Alpha, Red-Alpha, Green-Alpha, Blue-Alpha, and FULL Alpha ( full transparency ) ::

and the ability to use 256 different types of texture channels via the shader. If not, RGBA & being able to change the textureās quality per-texture would be just GREAT!!!

Once again, thank you SO kindly for the GREAT project!

@Aerion
Iām a bit confused. I donāt understand how you plan to be able to use 256 textures. Could you please explain?

Currently, I use 4 diffuse textures and 1 RGBA splat texture which dictates proportions of diffuse textures on each of 4 channels: R, G, B and A. So there are 256 possible āstrengthā contributions for each of the diffuse textures. I can imagine how a single channel can be split into 8 bands, and have basically binary blend states with a single bit: ON or OFF, but I still donāt see how to get ā256 different types of texture channelsā.

thanks

Iāve made such a technique for Tesseract, basically the possible 256 materials result by a 8 bit indices map, that could be a larger too of course, but 256 is quiet a lot and has to be in memory as well. It is quiet more complicated though, The indices management is prepared while brushing, since you donāt work with a splat-map per material.