Search code examples
javascriptthree.jsshader

Map image as texture to plane in a custom shader in THREE.js


I'm trying to load 2 images as textures, interpolate between them in the fragment shader and apply the color to a plane. Unfortunately, I can't even display a single texture.

I'm creating the plane and loading images as follows:

const planeGeometry = new THREE.PlaneBufferGeometry(imageSize * screenRatio, imageSize);

loader.load(
    "textures/IMG_6831.jpeg",
    (image) => {
        texture1 = new THREE.MeshBasicMaterial({ map: image });
        //texture1 = new THREE.Texture({ image: image });
    }
)

It displayed the image on the plane correctly when I used MeshBasicMaterial directly as a material for Mesh as normal. When trying to do the same in a custom shader I get only black color:

const uniforms = {
    texture1: { type: "sampler2D", value: texture1 },
    texture2: { type: "sampler2D", value: texture2 }
};

const planeMaterial = new THREE.ShaderMaterial({
    uniforms: uniforms,
    fragmentShader: fragmentShader(),
    vertexShader: vertexShader()
});

const plane = new THREE.Mesh(planeGeometry, planeMaterial);
scene.add(plane);

Shaders:

const vertexShader = () => {
    return `
        varying vec2 vUv; 

        void main() {
            vUv = uv; 

            vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
            gl_Position = projectionMatrix * modelViewPosition; 
        }
    `;
}

const fragmentShader = () => {
    return `
        uniform sampler2D texture1; 
        uniform sampler2D texture2; 
        varying vec2 vUv;

        void main() {
            vec4 color1 = texture2D(texture1, vUv);
            vec4 color2 = texture2D(texture2, vUv);
            //vec4 fColor = mix(color1, color2, vUv.y);
            //fColor.a = 1.0;
            gl_FragColor = color1;
        }
    `;
}

To fix it, I tried:

  • ensured that the plane is visible by shading it with a simple color
  • ensured that uv coordinates are passed to shader by visualizing them as color
  • ensured that texture1 and texture2 are defined before passing them to the shader
  • using THREE.Texture instead of THREE.MeshBasicMaterial
  • changing uniform type in js texture1: { type: "t", value: texture1 },

I think the problem might be in the part of passing the texture as uniform to the shader. I might be using the wrong type somewhere.. I'd appreciate any help!


Solution

  • Assuming loader is a TextureLoader then it calls you with a texture so

    loader.load(
        "textures/IMG_6831.jpeg",
        (texture) => {
            texture1 = texture;
        }
    )
    

    otherwise while it's not a bug, type is not used for three.js uniforms anymore.

    const planeGeometry = new THREE.PlaneBufferGeometry(1, 1);
    const loader = new THREE.TextureLoader();
    let texture1;
    loader.load(
        "https://i.imgur.com/KjUybBD.png",
        (texture) => {
            texture1 = texture;
            start();
        }
    );
    
    const vertexShader = () => {
        return `
            varying vec2 vUv; 
    
            void main() {
                vUv = uv; 
    
                vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
                gl_Position = projectionMatrix * modelViewPosition; 
            }
        `;
    }
    
    const fragmentShader = () => {
        return `
            uniform sampler2D texture1; 
            uniform sampler2D texture2; 
            varying vec2 vUv;
    
            void main() {
                vec4 color1 = texture2D(texture1, vUv);
                vec4 color2 = texture2D(texture2, vUv);
                //vec4 fColor = mix(color1, color2, vUv.y);
                //fColor.a = 1.0;
                gl_FragColor = color1;
            }
        `;
    }
    
    function start() {
      const renderer = new THREE.WebGLRenderer();
      document.body.appendChild(renderer.domElement);
      const scene = new THREE.Scene();
      
      const camera = new THREE.Camera();
      
      const uniforms = {
          texture1: { value: texture1 },
          texture2: { value: texture1 },
      };
    
      const planeMaterial = new THREE.ShaderMaterial({
          uniforms: uniforms,
          fragmentShader: fragmentShader(),
          vertexShader: vertexShader()
      });
    
      const plane = new THREE.Mesh(planeGeometry, planeMaterial);
      scene.add(plane);
    
      renderer.render(scene, camera);
    }
    <script src="https://cdn.jsdelivr.net/npm/three@0.111.0/build/three.min.js"></script>

    also TextureLoader returns a texture so if you're rendering continously in a requestAnimationFrame loop you can do write it like this

    const planeGeometry = new THREE.PlaneBufferGeometry(1, 1);
    const loader = new THREE.TextureLoader();
    const texture1 = loader.load("https://i.imgur.com/KjUybBD.png");
    const texture2 = loader.load("https://i.imgur.com/UKBsvV0.jpg");
    
    const vertexShader = () => {
        return `
            varying vec2 vUv; 
    
            void main() {
                vUv = uv; 
    
                vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
                gl_Position = projectionMatrix * modelViewPosition; 
            }
        `;
    }
    
    const fragmentShader = () => {
        return `
            uniform sampler2D texture1; 
            uniform sampler2D texture2; 
            varying vec2 vUv;
    
            void main() {
                vec4 color1 = texture2D(texture1, vUv);
                vec4 color2 = texture2D(texture2, vUv);
                gl_FragColor = mix(color1, color2, vUv.y);
            }
        `;
    }
    
    const renderer = new THREE.WebGLRenderer();
    document.body.appendChild(renderer.domElement);
    const scene = new THREE.Scene();
    
    const camera = new THREE.Camera();
    
    const uniforms = {
        texture1: { value: texture1 },
        texture2: { value: texture2 },
    };
    
    const planeMaterial = new THREE.ShaderMaterial({
        uniforms: uniforms,
        fragmentShader: fragmentShader(),
        vertexShader: vertexShader()
    });
    
    const plane = new THREE.Mesh(planeGeometry, planeMaterial);
    scene.add(plane);
    
    function render() {
      renderer.render(scene, camera);
      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
    <script src="https://cdn.jsdelivr.net/npm/three@0.111.0/build/three.min.js"></script>

    The textures will be blank but three.js will update them when the images have loaded.

    You might be interested in some more up to date tutorials