generateBlendedMaterial static method
Generate a material that blends together textures based on vertex height.
Inspired by http://www.chandlerprall.com/2011/06/blending-webgl-textures/
Usage:
// Assuming the textures are already loaded
final material = THREE.Terrain.generateBlendedMaterial([
{texture: THREE.ImageUtils.loadTexture('img1.jpg')},
{texture: THREE.ImageUtils.loadTexture('img2.jpg'), levels: -80, -35, 20, 50},
{texture: THREE.ImageUtils.loadTexture('img3.jpg'), levels: 20, 50, 60, 85},
{texture: THREE.ImageUtils.loadTexture('img4.jpg'), glsl: '1.0 - smoothstep(65.0 + smoothstep(-256.0, 256.0, vPosition.x)/// 10.0, 80.0, vPosition.z)'},
]);
This material tries to behave exactly like a MeshLambertMaterial other than the fact that it blends multiple texture maps together, although ShaderMaterials are treated slightly differently by Three.js so YMMV. Note that this means the texture will appear black unless there are lights shining on it.
List<TerrainTextures> textures
An array of objects specifying textures to blend together and how to blend
them. Each object should have a texture property containing a
THREE.Texture instance. There must be at least one texture and the first
texture does not need any other properties because it will serve as the
base, showing up wherever another texture isn't blended in. Other textures
must have either a levels property containing an array of four numbers
or a glsl property containing a single GLSL expression evaluating to a
float between 0.0 and 1.0. For the levels property, the four numbers
are, in order: the height at which the texture will start blending in, the
height at which it will be fully blended in, the height at which it will
start blending out, and the height at which it will be fully blended out.
The vec3 vPosition variable is available to glsl expressions; it
contains the coordinates in Three-space of the texel currently being
rendered.
Material material
An optional base material. You can use this to pick a different base
material type such as MeshStandardMaterial instead of the default
MeshLambertMaterial.
Implementation
static Material generateBlendedMaterial(List<TerrainTextures> textures, [Material? material]) {
// Convert numbers to strings of floats so GLSL doesn't barf on "1" instead of "1.0"
String glslifyNumber(num n) {
return n == n.toInt() && (kIsWeb && !kIsWasm)? '$n.0' : n.toString();
}
String declare = '',
assign = '';
Vector2 t0Repeat = textures[0].texture.repeat,
t0Offset = textures[0].texture.offset;
for (int i = 0, l = textures.length; i < l; i++) {
// Update textures
textures[i].texture.wrapS = textures[i].texture.wrapT = RepeatWrapping;
textures[i].texture.needsUpdate = true;
// Shader fragments
// Declare each texture, then mix them together.
declare += 'uniform sampler2D texture_$i;\n';
if (i != 0) {
final v = textures[i].levels, // Vertex heights at which to blend textures in and out
p = textures[i].glsl, // Or specify a GLSL expression that evaluates to a float between 0.0 and 1.0 indicating how opaque the texture should be at this texel
useLevels = v != null, // Use levels if they exist; otherwise, use the GLSL expression
tiRepeat = textures[i].texture.repeat,
tiOffset = textures[i].texture.offset;
if (useLevels) {
// Must fade in; can't start and stop at the same point.
// So, if levels are too close, move one of them slightly.
if (v[1] - v[0] < 1) v[0] -= 1;
if (v[3] - v[2] < 1) v[3] += 1;
}
// The transparency of the new texture when it is layered on top of the existing color at this texel is
// (how far between the start-blending-in and fully-blended-in levels the current vertex is) +
// (how far between the start-blending-out and fully-blended-out levels the current vertex is)
// So the opacity is 1.0 minus that.
final blendAmount = !useLevels ? p :'1.0 - smoothstep(${glslifyNumber(v[0])}, ${glslifyNumber(v[1])}, vPosition.z) + smoothstep(${glslifyNumber(v[2])}, ${glslifyNumber(v[3])}, vPosition.z)';
assign += '''
color = mix(
texture2D(
texture_$i,
MyvUv * vec2( ${glslifyNumber(tiRepeat.x)}, ${glslifyNumber(tiRepeat.y)} ) + vec2( ${glslifyNumber(tiOffset.x)}, ${glslifyNumber(tiOffset.y)})
),
color,
max( min($blendAmount, 1.0), 0.0)
);\n
''';
}
}
final fragBlend = '''
float slope = acos(max(min(dot(myNormal, vec3(0.0, 0.0, 1.0)), 1.0), -1.0));
diffuseColor = vec4( diffuse, opacity );
vec4 color = texture2D( texture_0, MyvUv * vec2(${glslifyNumber(t0Repeat.x)}, ${glslifyNumber(t0Repeat.y)}) + vec2(${glslifyNumber(t0Offset.x)}, ${glslifyNumber(t0Offset.y)})); // base
$assign
diffuseColor = color;
''';
final fragPars = '$declare\nvarying vec2 MyvUv;\nvarying vec3 vPosition;\nvarying vec3 myNormal;\n';
final mat = material ?? MeshLambertMaterial();
mat.onBeforeCompile = (WebGLParameters shader, WebGLRenderer renderer) {
// Patch vertexShader to setup MyUv, vPosition, and myNormal
shader.vertexShader = shader.vertexShader.replaceAll('#include <common>',
'varying vec2 MyvUv;\nvarying vec3 vPosition;\nvarying vec3 myNormal;\n#include <common>');
shader.vertexShader = shader.vertexShader.replaceAll('#include <uv_vertex>',
'MyvUv = uv;\nvPosition = position;\nmyNormal = normal;\n#include <uv_vertex>');
shader.fragmentShader = shader.fragmentShader.replaceAll('#include <common>', '$fragPars\n#include <common>');
shader.fragmentShader = shader.fragmentShader.replaceAll('#include <map_fragment>', fragBlend);
// Add our custom texture uniforms
for (int i = 0, l = textures.length; i < l; i++) {
shader.uniforms!['texture_$i'] = {
'type': 't',
'value': textures[i].texture,
};
}
};
return mat;
}