Custom cover() function for mimicking css background-size 'cover' for shaders, OrthographicCamera, TextureLoader, and PlaneBufferGeometry

Hi, I’ve been trying to research around but still could not figure out what in my code is preventing the images to act like ‘background-size:cover’

Codepen link: https://codepen.io/vchoo/pen/OJGMXaZ

<!--- IMAGESLOADED --->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.imagesloaded/4.1.4/imagesloaded.pkgd.js"></script>

<script>

    function cover( texture, aspect ) {

	  	var imageAspect = texture.image.width / texture.image.height;
		var scaleX = 1;
    	var scaleY = 1;
		
		console.log("Cover function:", texture.image.width);
		
		if (aspect > imageAspect) {
			// Canvas aspect ratio is wider than image aspect ratio
			scaleX = aspect / imageAspect;
		} else {
			// Canvas aspect ratio is taller than or equal to image aspect ratio
			scaleY = imageAspect / aspect;
		}

	  	texture.offset.set(0.5 * (1 - scaleX), 0.5 * (1 - scaleY));
    	texture.repeat.set(scaleX, scaleY);

	}

    const displacementSlider = function(opts) {

        let vertex = `
            varying vec2 vUv;
            void main() {
              vUv = uv;
              gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
            }
        `;

        let fragment = `

            varying vec2 vUv;

            uniform sampler2D currentImage;
            uniform sampler2D nextImage;

            uniform float dispFactor;

            void main() {

                vec2 uv = vUv;
                vec4 _currentImage;
                vec4 _nextImage;
                float intensity = 0.3;

                vec4 orig1 = texture2D(currentImage, uv);
                vec4 orig2 = texture2D(nextImage, uv);
                
                _currentImage = texture2D(currentImage, vec2(uv.x, uv.y + dispFactor * (orig2 * intensity)));

                _nextImage = texture2D(nextImage, vec2(uv.x, uv.y + (1.0 - dispFactor) * (orig1 * intensity)));

                vec4 finalTexture = mix(_currentImage, _nextImage, dispFactor);

                gl_FragColor = finalTexture;

            }
        `;

        let images = opts.images, image, sliderImages = [];
        let parent = opts.parent;
        
        let sizes = {
            w: window.innerWidth,
            h: window.innerHeight
        };
        
        let canvasWidth = sizes.w;
        let canvasHeight = sizes.h;
            
        let renderWidth = images[0].clientWidth;
        let renderHeight = images[0].clientHeight;
            

        let renderer = new THREE.WebGLRenderer({
            antialias: false,
        });
		
		let camera = new THREE.OrthographicCamera(
            renderWidth / -2,
            renderWidth / 2,
            renderHeight / 2,
            renderHeight / -2,
            1,
            1000
        );
		
		let scene = new THREE.Scene();
            

        renderer.setPixelRatio( window.devicePixelRatio );
        renderer.setClearColor( 0x23272A, 1.0 );
            
            
        renderer.setSize( renderWidth, renderHeight );
            
        parent.appendChild( renderer.domElement );

        let loader = new THREE.TextureLoader();
        loader.crossOrigin = "anonymous";
		
		let imageTextures = [];

        images.forEach( ( img ) => {

            let imageTexture = loader.load( img.getAttribute( 'src' ) + '?v=' + Date.now(), (texture) => {
				console.log("Image loaded:", imageTexture);
				
				cover( texture, canvasWidth / canvasHeight );
				scene.background = texture;
			} );
			
            imageTexture.magFilter = imageTexture.minFilter = THREE.LinearFilter;
            imageTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
            sliderImages.push( imageTexture );
			imageTextures.push(imageTexture);
        });
           

        camera.position.z = 1;

        let mat = new THREE.ShaderMaterial({
            uniforms: {
                dispFactor: { type: "f", value: 0.0 },
                currentImage: { type: "t", value: sliderImages[0] },
                nextImage: { type: "t", value: sliderImages[1] },
            },
            vertexShader: vertex,
            fragmentShader: fragment,
            transparent: true,
            opacity: 1.0
        });
            
		let aspectRatioCanvas = renderWidth / renderHeight;
        let aspectRatioImage = images[0].width / images[0].height;

        let scaleWidth = 1;
        let scaleHeight = 1;

        if (aspectRatioImage > aspectRatioCanvas) {
			scaleWidth = aspectRatioImage / aspectRatioCanvas;
		} else {
			scaleHeight = aspectRatioCanvas / aspectRatioImage;
		}
		
        let geometry = new THREE.PlaneBufferGeometry(
            renderWidth * scaleWidth,
            renderHeight * scaleHeight,
            1
        );
                        
        let object = new THREE.Mesh(geometry, mat);
        object.position.set(0, 0, 0);
		
        scene.add(object);

        let addEvents = function(){

            let pagButtons = Array.from(document.getElementById('pagination').querySelectorAll('a'));
            let isAnimating = false;

            pagButtons.forEach( (el) => {

                el.addEventListener('mouseenter', function() {

                    if( !isAnimating ) {

                        isAnimating = true;

                        document.getElementById('pagination').querySelectorAll('.active')[0].className = '';
                        this.className = 'active';

                        let slideId = parseInt( this.dataset.slide, 10 );

                        mat.uniforms.nextImage.value = sliderImages[slideId];
                        mat.uniforms.nextImage.needsUpdate = true;

                        TweenLite.to( mat.uniforms.dispFactor, 1, {
                            value: 1,
                            ease: 'Expo.easeInOut',
                            onComplete: function () {
                                mat.uniforms.currentImage.value = sliderImages[slideId];
                                mat.uniforms.currentImage.needsUpdate = true;
                                mat.uniforms.dispFactor.value = 0.0;
                                isAnimating = false;
                            }
                        });

                        let slideStatusEl = document.getElementById('slide-status');
                        let nextSlideStatus = document.querySelectorAll(`[data-slide-status="${slideId}"]`)[0].innerHTML;

                        TweenLite.fromTo( slideStatusEl, 0.5,
                            {
                                autoAlpha: 1,
                                y: 0
                            },
                            {
                                autoAlpha: 0,
                                y: 20,
                                ease: 'Expo.easeIn',
                                onComplete: function () {
                                    slideStatusEl.innerHTML = nextSlideStatus;

                                    TweenLite.to( slideStatusEl, 0.5, {
                                        autoAlpha: 1,
                                        y: 0,
                                        delay: 0.1,
                                    })
                                }
                            });

                    }

                });

            });

        };

        addEvents();

        window.addEventListener( 'resize' , function(e) {
			
			var aspect = window.innerWidth / window.innerHeight;

			camera.aspect = aspect;
			camera.updateProjectionMatrix();
			
            imageTextures.forEach(function(texture) {
				cover(texture, aspect);
			});

  			renderer.setSize( window.innerWidth, window.innerHeight );
        });

        let animate = function() {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
            
        animate();
    };

    imagesLoaded( document.querySelectorAll('img'), () => {

        // document.body.classList.remove('loading');

        const el = document.getElementById('slider');
        const imgs = Array.from(el.querySelectorAll('img'));
        new displacementSlider({
            parent: el,
            images: imgs
        });

    });

</script>

I’m using vanilla JS, no framework involved

Appreciate your help in advance!

Edit: I visited possible solutions but pretty much all are working with perspective camera.

Is there a way to integrate with orthographic camera?

I also suspect the THREE.PlaneBufferGeometry has something to do with the issue, but I’m new to three.js

1 Like

I didn’t take a full look at your code, but:

  • orthographic camera is a lot easier to do this for, because you have the width/height of the view already (the size of the Ortho camera bounds). So you only need to do some 2D addition/subtraction in a rectangle.
  • you don’t need to write a custom shader for this (I haven’t checked if yours is correct), your code will be simpler if you make a Mesh with PlaneGeometry and MeshBasicMaterial.
  • you have one renderer per image. You probably want a single renderer. Chrome is limited to 32 webgl contexts, the 33rd image will create a 33rd context, and this will crash the first context. The 34th context will crash the second one, and so on.

The best thing you can do is provide a live example for people to look at, to debug.

I’ve edited my post to include my test codepen; thank you so much

1 Like

Could you elaborate what does “to act like ‘background-size:cover’” mean?

Hi, I’m seeking for the images to extend to fill the width and height of the canvas at all times, regardless of canvas dimensions; but also keep the images’ aspect ratio intact and not distort the image proportions.

Thank you

Does this show the correct aspect:

https://codepen.io/boytchev/full/KKYVjPZ

2 Likes

Have a look at this short video from @akella

https://youtube.com/shorts/uaO9keAmC9Y?si=N6B3eBecNFq5D1IR

Might be what you’re looking for.

3 Likes

It does, thank you so much! Is it quick to have the image be centered within the canvas? The image is aligned left on smaller screens. I was trying to figure it out but to no avail so far : (

Greatly appreciate your time/help

Thank you so much! It’s been a really helpful reference!

In case you havn’t seen CSS cover before, see here:

1 Like

@Vickie_C there’s too much happening (and some things not doing anything) in your pen.

I recommend you simplify by positioning a Mesh (with PlaneGeometry), not a texture for scene background, based on this:

There you’ll find functions to implement similar to CSS object-fit: cover.

That will position exactly how you want using a PerspectiveCamera. It is even easier if you use an OrthographicCamera because then the fristum is rectangular. You can even fork the codepen from that post and start with that, then you can apply your shader to the texture of that Mesh (remove anything that is not needed in the shader like offset, etc).

Here’s the pen forked to use cover mode (see lines 90 to 92):

Video:

2 Likes