How to create a multiple textured terrain

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.

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

@Fyrestar could you elaborate a bit. I don’t get it :smiley:
What’s a “material” for you? Do you mean something like a material ID in a deferred rendering? How do you deal with blending if your “indices map” is discrete and each of 256 byte values corresponds to a “material index”? So many questions.
image

1 Like

This is what I mean by 256 textures:

https://forum.unity.com/threads/released-megasplat-a-256-texture-splat-mapping-system.441329/

Okay, took a while, but I think I understand now. So splatting is binary, meaning that you get to pick exactly 1 “material” per vertex, then inside the fragment shader you sample from 3 textures (1 per face vertex) and interpolate. It’s pretty neat, not too bad for GPU cache either, I think. Problem is only in the fact that you can’t do any blending aside from that between 2 vertices. It’s an interesting approach, I have considered it in the past, but I wanted smooth transitions, so I didn’t go for it. You can probably do smooth transitions with some trickery, like packing multiple copies of a texture into the “ArrayTexture”, but it wastes a lot of GPU memory.

Thanks for the link @Aerion

What i meant with materials is the map stack for a tile/material in the atlas (consisting of diffuse, normals, depth, roughness etc)

Actually storing in vertices is one way to go, but my technique is still based on a weightmap (one for all). Like i said it is about spatial distribution since you rather not need to always blend between all/many materials you have, what happens in the default splatting approach.

With mine you have a constant amount of texture fetches for a theoretically infinite number of materials. I’d say it works similar by storing indices of materials but doing it with maps, since i need a consistent material mapping over huge distances and can’t rely on the tesselation density, as well a varying falloffs still being possible.

Asuming the PBR maps (such as normals, depth, roughness etc) are already packed, with the regular splatting approach, for a number of 16 materials (or called layers there) it will take about 80 texture fetches, while with my spatially based approach it is a constant number of 14 fetches. The more fine details are added with stacked material patches and decal patches for details like puddles, branches, plants or rocks more supposed to be spot-details, but that’s a little more engine specific.

1 Like

Sounds pretty cool, is there a paper you have based it on, or some other source?

I don’t understand how you’re making the decision about which materials to blend out of many for a specific texel fragment. Personally, i thought about encoding material index and blend, naively that would take a channel each, so say: R: Index0, G: Blend0, B: Index1, A: Blend1. That gives you ability to blend (up to) 2 materials per given texel. Can pack more than 2 into Uint8 RGBA too.

Sorry i don’t have sources, i came up with this technique myself like the rest of the engine :sweat_smile:

Basically it gives the ability to blend between 4 materials per texel (+1 by using a ground base), this is why the processing on brushing has to be aided and ensure a padding of at least one texel if the channel uses 2 different materials, what happens at a “conflict” then is simply a replacement by the current brush. This isn’t really a limitation though especially when you base your terrain on biomes and use other techniques like i mentioned for additional details. The stacked material patches basically offer something similar the compositor for LOD based tiles would do by giving a much higher texel density by being a local area.

EDIT : So I’ve been working for weeks on this & to no avail.