Using PlaneGeometry with multiple textures

What I have
I am experimenting with procedural terrain generation. I have a class called TerrainGenerator, which takes width and segments as parameters and uses them to create a PlaneGeometry. Then, I iterate over all vertices and compute their height based on their x and y positions using simplex noise and other techniques. I have also implemented a biome system that assigns colors based on specific rules. After looping through all vertices, I use the setAttribute method to add colors to my PlaneGeometry.

// pseudocode
const geometry = new THREE.PlaneGeometry(width, width, segments, segments);
let vertices = this.geometry.attributes.position.array;

const colors = new Float32Array(vertexCount * 3);

for (let i = 0; i <= vertices.length; i += 3) {
    const z = noise(x, y);
    vertices[i + 2] =  z;

    const color = predictBiome(z, climate).color;
    colors[i] = color.r;
    colors[i + 1] = color.g;
    colors[i + 2] = color.b;
}

geometry.attributes.position.needsUpdate = true;
geometry.setAttribute('color', new BufferAttribute(colors, 3));
geometry.computeVertexNormals();

return this.geometry;

As a material for the mesh created with this geometry, I use a standard material:

const material = new THREE.MeshStandardMaterial({
            vertexColors: true,
            flatShading: false,
        });

The result looks like this:

As you can see colors are dynamically assigned based on some set of rules. Now i want to take it step futher and instead of using raw colors I want to add some textures.

What I tried to do
I can easily add some texture to whole PlaneGeomerty and create mesh, which looks like this:

But I’m stuck when it comes to placing multiple textures on it. I attempted to use shaders to blend textures. I even managed to implement a shader that dynamically selects one of two available textures.

Unlike the previous approach, which used MeshStandardMaterial with a texture passed as a map, this one uses ShaderMaterial. However, the problem is that the PlaneGeometry is no longer affected by the lights in my scene. From what I’ve read, I would need to manually implement lighting effects in the shader to make this work. Additionally, this approach makes it difficult to integrate my existing biome system abstraction (or at least, I am not aware of an efficient way to do so).

ChatGPT came up with using MultiMaterial, but from what I understand, this would require computing separate groups for every biome instance in my PlaneGeometry, which seems problematic.

Have you any tips or advice for this situation? Are there better solution about which i didn’t think about? Or maybe I should use one of mentioned approaches? I’m not really into computer graphics, and I work with Three.js very little so I could miss something obvious.

Instead of starting from scratch with ShaderMaterial, you could extend one of the existing materials.

  • You can do it manually with onBeforeCompile, as shown in this article:

Here you can find the shaders definition.

  • Or use this excellent plugin:
  • TSL is also an option:
5 Likes

I made custom class for material which overwrites onCompile method, and it works as I wanted.

Thank you for response.

1 Like

Here’s implementation of custom material which extends MeshStandard Material:

export default class CustomMaterial extends MeshStandardMaterial {
    texture: Texture;
    texture2: Texture;

    constructor(texture: Texture, texture2: Texture) {
        super();
        this.texture = texture;
        this.texture2 = texture2;

        console.log(this.texture);
    }

    onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer) => {
        shader.uniforms.uTexture = { value: this.texture };
        shader.uniforms.uTexture2 = { value: this.texture2 };
        shader.uniforms.uRepeat = { value: new THREE.Vector2(100, 100) };
        shader.uniforms.uHeightThreshold = { value: 200 };

        shader.vertexShader = shader.vertexShader.replace(
            "#include <common>",
            `
        #include <common>
        varying vec2 vUv;
        varying float vHeight;
        `
        );

        shader.vertexShader = shader.vertexShader.replace(
            "#include <uv_vertex>",
            `
        #include <uv_vertex>
        vUv = uv;
        vHeight = position.z;
        `
        );

        shader.fragmentShader = shader.fragmentShader.replace(
            "#include <common>",
            `
        #include <common>
        varying vec2 vUv;
        varying float vHeight;
        uniform sampler2D uTexture;
        uniform sampler2D uTexture2;
        uniform vec2 uRepeat;
        uniform float uHeightThreshold;
        `
        );

        shader.fragmentShader = shader.fragmentShader.replace(
            "#include <map_fragment>",
            `
        #include <map_fragment>
        vec2 repeatedUV = vUv * uRepeat;
        vec4 texColor;
        
        if (vHeight > uHeightThreshold) {
            texColor = texture(uTexture2, repeatedUV);  
        } else {
            texColor = texture(uTexture, repeatedUV); 
        }

        diffuseColor *= texColor;
        `
        );
        
    };
}

It is really simple, and as you can see, I use a constant threshold to change the texture. Also, it can only take two textures. I will improve this code for myself, but here I just want to show how I’ve done it, especially when it comes to replacing parts of shaders if someone else needs this. Here’s the final effect::

1 Like

If you want a smoother look, you ca also look into using triplanar mapping.

1 Like