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.

Something like this image:
[https://d33wubrfki0l68.cloudfront.net/16a96379a5ccaea113df2e8d89fdbe84e0c61b4e/51011/img/extensions/materials/terrain.jpg]

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:
Tile Blend Shader Experiment

This is my fragment shader:

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); 

} 

Please note this is my first shader coding experiment everā€¦ :wink:

5 Likes

Thanks for sharing your code! :+1:

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.

My shader codes:
`

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

<script id="tileblend_fragment" type="x-shader/x-vertex"> 
	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>

<script id="normalmap_pars_fragment" type="x-shader/x-vertex"> 

#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

terrainMaterial.onBeforeCompile = shader => {
	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};
	

	shader.fragmentShader = document.getElementById( 'tileblend_pars_fragment'   ).textContent + shader.fragmentShader
	shader.fragmentShader = 
	shader.fragmentShader.replace(
  		'#include <map_fragment>', 
  		document.getElementById( 'tileblend_fragment'   ).textContent
	)
	shader.fragmentShader = 
	shader.fragmentShader.replace(
  		'#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! :slight_smile: 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:
image
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.

preview_2048
sand:
sand
dirt:
dirt
rock:
rock
grass:
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. :slight_smile:

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 ) ::


splatmap


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!!! :slight_smile:

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

@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 :mermaid:

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.