GLSL shader: Interpolate between more than two textures

16,298

Solution 1

mix() is really just a convenience function for something you can easily write yourself. The definition is:

mix(v1, v2, a) = v1 * (1 - a) + v2 * a

Or putting it differently, it calculates a weighted average of v1 and v2, with two weights w1 and w2 that are float values between 0.0 and 1.0 meeting the constraint w1 + w2 = 1.0:

v1 * w1 + v2 * w2

You can directly generalize this to calculate a weighted average of more than 2 inputs. For example, for 3 inputs v1, v2 and v3, you would use 3 weights w1, w2 and v3 meeting the constraint w1 + w2 + w3 = 1.0, and calculate the weighted average as:

v1 * w1 + v2 * w2 + v3 * w3

For your example, determine the weights you want to use for each of the 3 textures, and then use something like:

weightIce = ...;
weightStone = ...;
weightGrass = 1.0 - weightIce - weightStone;
color = texture2D(ice_layer_tex, texcoord) * weightIce +
        texture2D(stone_layer_tex, texcoord) * weightStone +
        texture2D(grass_layer_tex, texcoord) * weightGrass;

Solution 2

No, according to the GLSL documentation for mix() there are only overloads for interpolation between two parameters.

Would it be acceptable to you to just interpolate "ice" and "stone" then mix the result with the "grass" texture?

vec4 ice_color   = texture2D(ice_layer_tex,   texcoord);
vec4 stone_color = texture2D(stone_layer_tex, texcoord);
vec4 grass_color = texture2D(grass_layer_tex, texcoord);

vec4 tmp = mix(ice_color, stone_color, pct);
vec4 final_color = mix(tmp, grass_color, pct);

Solution 3

The other answers have already provided solutions for the genralized mix() function you asked for. But I'd recommend using a different approach, since you explicitely wrote about an "interpolation order (ice, stone, grass)". In that case, you don't need arbitrary weights for each element, you only mix neighboring ones, like ice+stone or stone+grass, but never ice+grass or ice+stone+grass. If that is the case, you can simply use 3D textures and use (tri)linear filtering. Just use each of your 2D texture as a slice in the 3D texture. The first two texcoords can stay as they are, and the third can be directly used to select an arbitrary blending between two neighboring slices. Since texcoords are always in the range [0,1], you just have to map your range to that interval. The "center" of the i-th slice will lie at

p=i/num_layers + 1/(2*num_layers)

Say you have those 3 slices for ice, stone and grass. So you get

0/3+1/6 = 0.16667       100% ice  
1/3+1/6 = 0.5           100% stone
2/3+1/6 = 0.83333       100% grass

and arbirtrary linear blends between neighboring layers just inbetween, like

1/3 = 0.3333            50% ice + 50% stone  
      0.6               70% stone  + 30% grass
...
Share:
16,298
T_01
Author by

T_01

Updated on June 04, 2022

Comments

  • T_01
    T_01 almost 2 years

    I've implemented a heightmap in OpenGL. For now it is just a sine/cosine curved terrain. At the moment I am interpolating between the white "ice" and the darker "stone" texture. This is done like this:

    color = mix(texture2D(ice_layer_tex, texcoord), texture2D(stone_layer_tex, texcoord), (vertex.y + amplitude) / (amplitude * 2))
    

    The result:

    from top

    from bottom

    It works fine, but what could I do if I want to add more textures, for example a grass texture, so that the interpolation order is "ice, stone, grass"? I think, there isn't a function like mix(sampler2D[], percentages[])? How could I write a GLSL method following this logic?