Skip to content

AxiomeCG/r3f-image-cover

Repository files navigation

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.

Showcase of the result

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);
}