React Three Fiber and three.js Image Cover
React Three Fiber and three.js Image Cover
The aim of this repo is to implement an efficient and reliable way to put as a texture pictures in PlaneGeometries, without any stretching.
The effect is similar to the property background-size: cover; in CSS, but for a three.js usage.
The code is made with react-three-fiber, but all is about creating a ShaderMaterial corresponding to the need and to precalculate the uniforms CPU side.
Process
For people not used to react-three-fiber, I don’t want you to get overwhelmed so here is what you need to calculate in native three.js to get the exact same result.
The main logic is fully contained here:
const calculateScaleFactors = (texture: Texture, containerSize: Vector2) => {
  const containerAspectRatio = containerSize.x / containerSize.y;
  const imageAspectRatio = texture.image.width / texture.image.height;
  let scaleFactorX = 1;
  let scaleFactorY = 1;
  const landscapeFactor = imageAspectRatio / containerAspectRatio;
  const portraitFactor = containerAspectRatio / imageAspectRatio;
  const isLandscapeModeContainer = containerAspectRatio >= 1;
  const isContainerRatioStronger = containerAspectRatio >= imageAspectRatio;
  if (isContainerRatioStronger) {
    scaleFactorY = isLandscapeModeContainer ? landscapeFactor : portraitFactor;
  } else {
    scaleFactorX = isLandscapeModeContainer ? landscapeFactor : portraitFactor;
  }
  return {scaleFactorX, scaleFactorY}
}
where containerSize is the width and height of the PlaneGeometry in three.js units.
Once the scale factors are calculated, you can pass them as uniform in a regular ShaderMaterial as follow:
const uniforms = {
      uTexture: {
        value: texture
      },
      uScaleFactorX: {
        value: scaleFactorX
      },
      uScaleFactorY: {
        value: scaleFactorY
      }
}
Nothing is done in the vertexShader, only passing the UVs to the fragment shader.
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
// fragment shader
uniform sampler2D uTexture;
uniform float uScaleFactorX;
uniform float uScaleFactorY;
varying vec2 vUv;
void main() {
    vec2 st = (vUv * 2.0 - 1.0); //Centering the UVs space between -1 and 1 to apply the scaling.
    
    // Scale the UVs so the width or the height is overflowing the plane.
    // The other stays at 1 so it fully takes place on the dimension of the plane.
    st.x *= uScaleFactorX; 
    st.y *= uScaleFactorY;
    // Put back the UVs space between """0 and 1 range""" so the texture image is covering the plane nicely.
    st = st * 0.5 + 0.5;
    gl_FragColor = texture(uTexture, st);
}