3

I want my texture to have the same behaviour than the "background-size:cover" css property.

I'd like to work with uvs coordinates. I looked at this answer and start to work on a solution : Three.js Efficiently Mapping Uvs to Plane

I try to have the same dimension/position planes that some div of my DOM. This is what I want :

enter image description here

And this is the result I get with this code : the dimension and position are good, the ratio of my texture looks good too but it seems like there's a scale issue :

            let w = domElmt.clientWidth / window.innerHeight;
            let h = domElmt.clientHeight / window.innerHeight;
            geometry = new THREE.PlaneGeometry(w, h);

            var uvs = geometry.faceVertexUvs[ 0 ];
            uvs[ 0 ][ 0 ].set( 0, h );
            uvs[ 0 ][ 1 ].set( 0, 0 );
            uvs[ 0 ][ 2 ].set( w, h );
            uvs[ 1 ][ 0 ].set( 0, 0 );
            uvs[ 1 ][ 1 ].set( w, 0 );
            uvs[ 1 ][ 2 ].set( w, h );

            tex = new THREE.TextureLoader().load('image.jpg'));
            tex.wrapS = tex.wrapT = THREE.RepeatWrapping;

            material = new THREE.MeshBasicMaterial( { map: tex } );
            mesh = new THREE.Mesh( geometry, material );

enter image description here

Should I play with the repeat attribute of my texture or can I fully made this behaviour using uvs ? Thank you

Michaël
  • 489
  • 1
  • 5
  • 21

2 Answers2

2

https://en.wikipedia.org/wiki/UV_mapping

UV mapping values range from 0 to 1, inclusive, and represent a percentage mapping across your texture image.

You're using a ratio of the div size vs the window size, which is likely much smaller than 1, and would result in the "zoomed in" effect you're seeing.

For example, if your w and h result in the value 0.5, then The furthest top-right corner of the mapped texture will be the exact center of the image.

background-style: cover:

Scales the image as large as possible without stretching the image. If the proportions of the image differ from the element, it is cropped either vertically or horizontally so that no empty space remains.

In other words, it will scale the image based on the size of the short side, and crop the rest. So let's assume you have a nice 128x512 image, and a 64x64 space. cover would scale the width of 128 down to 64 (a scale factor of 0.5), so multiply 512 by 0.5 to get the new height (128). Now your w would still be 1, but your h will be 128 / 512 = 0.25. Your texture will now fit to the width, and crop the height.

You'll need to perform this calculation for each image-to-container size relationship to find the proper UVs, keeping in mind that the scaling is always relevant to the short side.

TheJim01
  • 8,411
  • 1
  • 30
  • 54
  • Thanks for the answer. I’m using a ratio of the div size and the height of my window because my canvas fits to the viewport size : that way I know « 1 » is 100% of the height of my canvas when I set the geometry dimension and I have the exact same dimension than my DOM element. I just replaced w and h on my uvs by 1. It results that my texture is now fitting entirely inside my geometry : https://image.noelshack.com/fichiers/2019/25/1/1560783211-capture-d-ecran-2019-06-17-a-16-53-19.png – Michaël Jun 17 '19 at 14:53
  • @MichaëlGarcia I've edited my answer to expand on how `cover` does its scaling, and how you might apply it to your UVs. – TheJim01 Jun 17 '19 at 17:48
  • I know have my texture at the good ratio inside my geometry. The only thing that is missing is that it's not center vertically. I fixed that last issue using the texture.offset.y property. But can I fix this using only uvs coordinates ? Thanks (also, 128 / 512 = 0.25) – Michaël Jun 18 '19 at 09:34
2

You don't need to generate UVs, you can just use texture.repeat and texture.offset

    const aspectOfPlane = planeWidth / planeHeight;
    const aspectOfImage = image.width / image.height;
    let yScale = 1;
    let xScale = aspectOfPlane / aspectOfImage;
    if (xScale > 1) { // it doesn't cover so based on x instead
      xScale = 1;
      yScale = aspectOfImage / aspectOfPlane;
    }
    texture.repeat.set(xScale, yScale);
    texture.offset.set((1 - xScale) / 2, (1 - yScale) / 2); 

'use strict';

/* global THREE */

async function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 75;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 50;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 4;

  const scene = new THREE.Scene();

  const loader = new THREE.TextureLoader();
  function loadTexture(url) {
    return new Promise((resolve, reject) => {
      loader.load(url, resolve, undefined, reject);
    });
  }
  
  const textures = await Promise.all([
    "https://i.imgur.com/AyOufBk.jpg",
    "https://i.imgur.com/ZKMnXce.png",
    "https://i.imgur.com/TSiyiJv.jpg",
    "https://i.imgur.com/v38pV.jpg",
  ].map(loadTexture));

  const geometry = new THREE.PlaneBufferGeometry(1, 1);
  const material = new THREE.MeshBasicMaterial({map: textures[0]});
  const planeMesh = new THREE.Mesh(geometry, material);
  scene.add(planeMesh);
  
  let texIndex = 0;
  function setTexture() {
    const texture = textures[texIndex];
    texIndex = (texIndex + 1) % textures.length;

    // pick and random width and height for plane
    const planeWidth = rand(1, 4);
    const planeHeight = rand(1, 4);
    planeMesh.scale.set(planeWidth, planeHeight, 1);
 
    const image = texture.image;
    
    const aspectOfPlane = planeWidth / planeHeight;
    const aspectOfImage = image.width / image.height;
    let yScale = 1;
    let xScale = aspectOfPlane / aspectOfImage;
    if (xScale > 1) { // it doesn't cover so based on x instead
      xScale = 1;
      yScale = aspectOfImage / aspectOfPlane;
    }
    texture.repeat.set(xScale, yScale);
    texture.offset.set((1 - xScale) / 2, (1 - yScale) / 2);    
    material.map = texture;
  }
  
  setTexture();
  setInterval(setTexture, 1000);

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  function render(time) {
    time *= 0.001;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

function rand(min, max) {
  if (max === undefined) {
    max = min;
    min = 0;
  }
  return Math.random() * (max - min) + min;
}

main();
body {
  margin: 0;
}
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas id="c"></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r105/three.min.js"></script>

texture.repeat and texture.offset are really just applied to the UVs so if you really want UVs it's

newU = u * repeat.x + offset.x;
newV = v * repeat.y + offset.y;

so using the code above

offsetX = (1 - xScale) / 2;
offsetY = (1 - yScale) / 2;
u0 = offsetX;
v0 = offsetY;
u1 = offsetX + xScale;
v1 = offsetY + yScale;

so

        var uvs = geometry.faceVertexUvs[ 0 ];
        uvs[ 0 ][ 0 ].set( u0, v1 );
        uvs[ 0 ][ 1 ].set( u0, v0 );
        uvs[ 0 ][ 2 ].set( u1, v1 );
        uvs[ 1 ][ 0 ].set( u0, v0 );
        uvs[ 1 ][ 1 ].set( u1, v0 );
        uvs[ 1 ][ 2 ].set( u1, v1 );
gman
  • 100,619
  • 31
  • 269
  • 393