How to repeat image texture on the mesh material based on the texture's actual size

UPDATE

I have updated code as following, but not able to set number of repetitions’ dynamically. please guide in this. numbers of rows and columns are fixed trying to make it dynamic as per the tile image texture aspect ratio and wall size.

function loadTextureAndSetRepeat({ element, rotateAngle = 0 }) {
  let surfaceData = element.userData;
  let randomIndex = Math.floor(Math.random() * tilesData.length);
  let currentTexture = tilesData[randomIndex];

  const imageUrls = currentTexture.imgUrl;
  const groutThickness = 5; // Adjust grout thickness as needed

  const createTextureWithGrout = (images) => {
    // Calculate the combined texture dimensions
    const tileWidth = images[0].width;
    const tileHeight = images[0].height;
    const columns = 4;
    const rows =4;
    const combinedWidth = columns * (tileWidth + groutThickness) - groutThickness;
    const combinedHeight = rows * (tileHeight + groutThickness) - groutThickness;

    // Create a canvas to draw the combined texture
    const canvas = document.createElement('canvas');
    canvas.width = combinedWidth;
    canvas.height = combinedHeight;
    const context = canvas.getContext('2d');

    // Draw each image with grout lines
    images.forEach((image, index) => {
      const col = index % columns;
      const row = Math.floor(index / columns);
      const x = col * (tileWidth + groutThickness);
      const y = row * (tileHeight + groutThickness);

      // Draw the tile image
      context.drawImage(image, x, y, tileWidth, tileHeight);

      // Draw vertical grout line (if not the last column)
      if (col < columns - 1) {
        context.fillStyle = 'red'; // Adjust grout color as needed
        context.fillRect(x + tileWidth, y, groutThickness, tileHeight);
      }

      // Draw horizontal grout line (if not the last row)
      if (row < rows - 1) {
        context.fillStyle = 'red'; // Adjust grout color as needed
        context.fillRect(x, y + tileHeight, tileWidth, groutThickness);
      }
    });

    // Create and return the Three.js texture
    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
    return texture;
  };

  const loadImages = (urls) => {
    return Promise.all(urls.map((url) => {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => resolve(image);
        image.onerror = (error) => reject(error);
        image.src = url;
      });
    }));
  };

  loadImages(imageUrls)
    .then((images) => {
      const combinedTexture = createTextureWithGrout(images);

      element.material.map = combinedTexture;
      element.material.needsUpdate = true;

      renderer.render(scene, camera);
    })
    .catch((error) => {
      console.error('Error loading images:', error);
    });
}

current output

any suggestions or guidance please :slight_smile: Thanks in advance

UPDATE :

implemented some modifications in repetition of tiles texture & grout creation. facing issue as images are getting stretched a bit.

also on camera movement changes how can we restrict the wall mesh movement ?

function loadTextureAndSetRepeat({ element, rotateAngle = 0 }) {
  let surfaceData = element.userData;
  let randomIndex = Math.floor(Math.random() * tilesData.length);
  let currentTexture = tilesData[randomIndex];

  const imageUrls = currentTexture.imgUrl;

  // Convert surfaceData width and height from feet to meters
  const feetToMeters = 0.3048;
  const wallWidthMeters = surfaceData.width * feetToMeters;
  const wallHeightMeters = surfaceData.height * feetToMeters;

  const createTextureWithGrout = (images, widthMeters, heightMeters) => {
    const tileWidthMeters = 6; // Tile width in meters (600mm)
    const tileHeightMeters = 6; // Tile height in meters (600mm)
    const groutThicknessMeters = 0.2; // Grout thickness in meters (5mm)

    const columns = Math.ceil(widthMeters / tileWidthMeters);
    const rows = Math.ceil(heightMeters / tileHeightMeters);

    // Create a canvas to draw the combined texture
    const canvas = document.createElement('canvas');
    canvas.width = Math.ceil((tileWidthMeters * columns) * 100); // Convert to pixels
    canvas.height = Math.ceil((tileHeightMeters * rows) * 100); // Convert to pixels
    const context = canvas.getContext('2d');

    // Check if images are loaded before proceeding
    if (images.some(image => !image.complete || image.naturalWidth === 0)) {
      throw new Error('One or more images failed to load.');
    }

    // Draw each image with grout lines
    for (let col = 0; col < columns; col++) {
      for (let row = 0; row < rows; row++) {
        const x = col * tileWidthMeters * 100;
        const y = row * tileHeightMeters * 100;
        const image = images[(row * columns + col) % images.length];

        // Create a temporary canvas to draw the tile with grout
        const tileCanvas = document.createElement('canvas');
        tileCanvas.width = tileWidthMeters * 100;
        tileCanvas.height = tileHeightMeters * 100;
        const tileContext = tileCanvas.getContext('2d');

        // Draw the tile image on the temporary canvas
        tileContext.drawImage(image, 0, 0, tileCanvas.width, tileCanvas.height);

        // Draw the grout lines on the tile image
        tileContext.fillStyle = 'rgb(255, 0, 0)'; // Red grout color

        // Draw vertical grout lines
        tileContext.fillRect(tileCanvas.width - groutThicknessMeters * 100, 0, groutThicknessMeters * 100, tileCanvas.height);

        // Draw horizontal grout lines
        tileContext.fillRect(0, tileCanvas.height - groutThicknessMeters * 100, tileCanvas.width, groutThicknessMeters * 100);

        // Draw the modified tile image on the main canvas
        context.drawImage(tileCanvas, x, y);
      }
    }

    // Create the Three.js texture from the canvas
    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.rotation = Math.PI *  tileTextureRotation;
    texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
    return texture;
  };

  const loadImages = (urls) => {
    return Promise.all(urls.map((url) => {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.crossOrigin = "anonymous"; // To handle CORS issues
        image.onload = () => resolve(image);
        image.onerror = (error) => reject(error);
        image.src = url;
      });
    }));
  };

  loadImages(imageUrls)
    .then((images) => {
      const combinedTexture = createTextureWithGrout(images, wallWidthMeters, wallHeightMeters);

      // Assign texture to element material in Three.js
      element.material.map = combinedTexture;
      element.material.needsUpdate = true;

      // Render scene
      renderer.render(scene, camera);
    })
    .catch((error) => {
      console.error('Error loading images:', error);
    });
}

walls & scene creation

// Function to create a skybox
const createSkybox = async (layoutType, texturesArray) => {
  try {
    const textures = await Promise.all(texturesArray.map(loadTexture));
    const materials = textures.map(
      (texture) =>
        new THREE.MeshBasicMaterial({
          map: texture,
          side: THREE.BackSide, // Make sure the textures are on the inside
          transparent: true,
          opacity: 1,
          blending:
            layoutType === "base"
              ? THREE.NormalBlending
              : THREE.MultiplyBlending,
        })
    );
    const geometry = new THREE.BoxGeometry(100, 100, 100); // Large enough to act as a skybox
    const skybox = new THREE.Mesh(geometry, materials);
    skybox.rotation.y = -Math.PI / 2;
    return skybox;
  } catch (error) {
    console.error("Error loading textures for skybox:", error);
  }
};

// Load and add skyboxes to the scene
Promise.all([
  createSkybox("base", texturesMap.transparent),
  createSkybox("glossy", texturesMap.matt),
  createSkybox("matt", texturesMap.glossy),
]).then((skyboxes) => {
  [transparentSkybox, mattSkybox, glossySkybox] = skyboxes;

  // Add skyboxes to the scene
  if (mattSkybox) {
    group.add(mattSkybox);
    mattSkybox.visible = false; // Set the matt skybox invisible by default
  }
  if (glossySkybox) {
    group.add(glossySkybox);
    glossySkybox.visible = true; // Set the glossy skybox visible by default
  }
  if (transparentSkybox) {
    group.add(transparentSkybox);
    transparentSkybox.visible = true; // Set the transparent skybox visible by default
  }
  const ambientLight = new THREE.AmbientLight(0xffffff, informazione.intensity);
  scene.add(group);

  scene.add(ambientLight);

  //create walls
  for (const element of data) {
    if (element.face.toLowerCase() === "wall") {
      const wallGeometry = new THREE.PlaneGeometry(
        element.width,
        element.height
      );
      const wallMaterial = new THREE.MeshBasicMaterial({
        map: defaultFloorTexture, // Use default texture
        side: THREE.DoubleSide,
        depthTest: true,
      });
      const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial);
      wallMesh.userData = element;
      wallMesh.position.set(
        element["position-x"],
        element["position-y"],
        element["position-z"]
      );
      wallMesh.rotation.y = element["rotation-y"];
      wallMesh.rotation.x = element.rotateX;
      group.add(wallMesh);
    }
    if (element.face.toLowerCase() === "floor") {
      const floorGeometry = new THREE.PlaneGeometry(
        element.width,
        element.height
      );
      const floorMaterial = new THREE.MeshBasicMaterial({
        map: defaultFloorTexture, // Use default texture
        side: THREE.DoubleSide,
        depthTest: true,
      });
      const floorMesh = new THREE.Mesh(floorGeometry, floorMaterial);
      let dataObj = element;
      floorMesh.userData = dataObj;
      floorMesh.position.set(
        element["position-x"],
        element["position-y"],
        element["position-z"]
      );
      floorMesh.rotation.x = Math.PI / 2;
      group.add(floorMesh);
    }
  }
  scene.add(group);
  animate();
});

another issue is when I try to apply texture on floor, getting issues and texture isn’t getting applied

Thanks

@manthrax I took reference from this post.

I tried by changing different images, it almost works correctly.
demo : Glitch :・゚✧

EDIT : it works for square tile textures :slight_smile:

can you please guide how can we implement that texture repetition in following function with grouts ?

function loadTextureAndSetRepeat({ element, rotateAngle = 0 }) {
  let surfaceData = element.userData;
  let randomIndex = Math.floor(Math.random() * tilesData.length);
  let currentTexture = tilesData[randomIndex];

  const imageUrls = currentTexture.imgUrl;

  // Convert surfaceData width and height from feet to meters
  const feetToMeters = 0.3048;
  const wallWidthMeters = surfaceData.width * feetToMeters;
  const wallHeightMeters = surfaceData.height * feetToMeters;

  const createTextureWithGrout = (images, widthMeters, heightMeters) => {
    const tileWidthMeters = 6; // Tile width in meters (600mm)
    const tileHeightMeters = 6; // Tile height in meters (600mm)
    const groutThicknessMeters = 0.2; // Grout thickness in meters (5mm)

    const columns = Math.ceil(widthMeters / tileWidthMeters);
    const rows = Math.ceil(heightMeters / tileHeightMeters);

    // Create a canvas to draw the combined texture
    const canvas = document.createElement('canvas');
    canvas.width = Math.ceil((tileWidthMeters * columns) * 100); // Convert to pixels
    canvas.height = Math.ceil((tileHeightMeters * rows) * 100); // Convert to pixels
    const context = canvas.getContext('2d');

    // Check if images are loaded before proceeding
    if (images.some(image => !image.complete || image.naturalWidth === 0)) {
      throw new Error('One or more images failed to load.');
    }

    // Draw each image with grout lines
    for (let col = 0; col < columns; col++) {
      for (let row = 0; row < rows; row++) {
        const x = col * tileWidthMeters * 100;
        const y = row * tileHeightMeters * 100;
        const image = images[(row * columns + col) % images.length];

        // Create a temporary canvas to draw the tile with grout
        const tileCanvas = document.createElement('canvas');
        tileCanvas.width = tileWidthMeters * 100;
        tileCanvas.height = tileHeightMeters * 100;
        const tileContext = tileCanvas.getContext('2d');

        // Draw the tile image on the temporary canvas
        tileContext.drawImage(image, 0, 0, tileCanvas.width, tileCanvas.height);

        // Draw the grout lines on the tile image
        tileContext.fillStyle = 'rgb(255, 0, 0)'; // Red grout color

        // Draw vertical grout lines
        tileContext.fillRect(tileCanvas.width - groutThicknessMeters * 100, 0, groutThicknessMeters * 100, tileCanvas.height);

        // Draw horizontal grout lines
        tileContext.fillRect(0, tileCanvas.height - groutThicknessMeters * 100, tileCanvas.width, groutThicknessMeters * 100);

        // Draw the modified tile image on the main canvas
        context.drawImage(tileCanvas, x, y);
      }
    }

    // Create the Three.js texture from the canvas
    const texture = new THREE.CanvasTexture(canvas);
    texture.wrapS = THREE.RepeatWrapping;
    texture.wrapT = THREE.RepeatWrapping;
    texture.rotation = Math.PI *  tileTextureRotation;
    texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
    return texture;
  };

  const loadImages = (urls) => {
    return Promise.all(urls.map((url) => {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.crossOrigin = "anonymous"; // To handle CORS issues
        image.onload = () => resolve(image);
        image.onerror = (error) => reject(error);
        image.src = url;
      });
    }));
  };

  loadImages(imageUrls)
    .then((images) => {
      const combinedTexture = createTextureWithGrout(images, wallWidthMeters, wallHeightMeters);

      // Assign texture to element material in Three.js
      element.material.map = combinedTexture;
      element.material.needsUpdate = true;

      // Render scene
      renderer.render(scene, camera);
    })
    .catch((error) => {
      console.error('Error loading images:', error);
    });
}
function loadTextureAndSetRepeat({ element }) {
  const textureLoader = new THREE.TextureLoader();


  // repetition with too small size


  // const vertexShader = `
  //   varying vec3 vWorldPosition;
  //   varying vec3 vWorldNormal;
  //   varying vec3 vViewDirection;

  //   void main() {
  //     vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  //     vWorldPosition = worldPosition.xyz;
  //     vWorldNormal = normalize(mat3(modelMatrix) * normal);
  //     vViewDirection = cameraPosition - vWorldPosition;

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

  // const fragmentShader = `
  //   uniform sampler2D map;
  //   uniform vec3 textureScale;

  //   varying vec3 vWorldPosition;
  //   varying vec3 vWorldNormal;
  //   varying vec3 vViewDirection;

  //   void main() {
  //     vec3 scaledCoords = vWorldPosition * textureScale;

  //     vec2 uv;
  //     if (abs(vWorldNormal.y) > 0.5) {
  //       uv = scaledCoords.xz;
  //     } else if (abs(vWorldNormal.x) > 0.5) {
  //       uv = scaledCoords.yz;
  //     } else {
  //       uv = scaledCoords.xy;
  //     }

  //     vec3 albedo = texture2D(map, uv).rgb;
  //     gl_FragColor = vec4(albedo, 1.0);
  //   }
  // `;

  //single tile covers wall 
  const vertexShader = `
  varying vec2 vUv;

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

  const fragmentShader = `
  uniform sampler2D map;
  uniform vec2 textureScale;

  varying vec2 vUv;

  void main() {
    vec2 scaledUv = vUv * textureScale;
    vec3 albedo = texture2D(map, scaledUv).rgb;
    gl_FragColor = vec4(albedo, 1.0);
  }
`;

  // Get the array of image URLs from tilesData
  let imgURLs = tilesData[0].imgUrl;

  // Function to load texture and apply to the element
  function applyTextures() {
    const currentTexture = imgURLs[Math.floor(Math.random() * imgURLs.length)];

    // Create a shader material for the current texture
    const uniforms = {
      textureScale: { value: new THREE.Vector3(1, 1, 1) },
      map: { value: null }, // Placeholder for the texture
    };
    const shaderMaterial = new THREE.ShaderMaterial({
      uniforms,
      vertexShader,
      fragmentShader,
      side: THREE.DoubleSide, // Make material double-sided
    });

    // Load texture and assign it to the material
    textureLoader.load(currentTexture, (texture) => {
      texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
      shaderMaterial.uniforms.map.value = texture;
      shaderMaterial.needsUpdate = true; // Ensure material knows it needs update

      // Copy the geometry of the intersected element
      const copiedGeometry = element.geometry.clone();

      // Create a new mesh with the copied geometry and shader material
      const newMesh = new THREE.Mesh(copiedGeometry, shaderMaterial);

      // Set the position, rotation, and scale of the new mesh
      newMesh.position.copy(element.position);
      newMesh.rotation.copy(element.rotation);
      newMesh.scale.copy(element.scale);
      newMesh.userData = element.userData; // Preserve any userData

      // Add the new mesh to the same parent
      if (element.parent) {
        element.parent.add(newMesh);
      }

      // Hide the old element
      element.visible = false;

      // Render the scene
      renderer.render(scene, camera);
    });
  }

  // Apply textures
  applyTextures();
}

//single tile covers wall

// repetition with too small size

someone please guide, as I am unaware about the shaders

1 Like

Thanks @manthrax

I will surely try this changes of grout, can you please guide for following

how to scale it correctly.

 const uniforms = {
      textureScale: { value: new THREE.Vector3(0.1, 0.1, 0.1) },
      map: { value: null }, // Placeholder for the texture
    };

if we set 0.1 in vector. it’s displayed as following. but as far as I understood it should be THREE.Vector3(1, 1, 1)

Thank in advance :slight_smile:

can we change the grout position according to the textureScale and ratio ?

currently it’s square. but we need to change as per the img texture.

as per the current code :

reference example :

some sample img references : 300_600, 300_450, 1200_1200, 200_1200, 600_1200

Hi
Is your problem solved?
my current problem is this

can you help me?

@Parisa_Shahbazi please refer this post : Texture repetition & Drawing lines issue with GLSL shaders

this issue was resolved with the help of @PavelBoytchev @manthrax and @Chaser_Code.

calculated the scale factor for that as following and passed in the fragment shader

script.js

const scaleFactor = surfaceData.face === "wall" ? 0.025 : 0.012;

 scaleFactor: { value: scaleFactor / zoomFactor },

fshaders.js


uniform float tileWidth;
uniform float tileHeight;
uniform float planeWidth;
uniform float planeHeight;
uniform float scaleFactor;

 float repeatX = (planeWidth / tileWidth) * scaleFactor;
 float repeatY = (planeHeight / tileHeight) * scaleFactor;

live code link : Glitch :・゚✧

1 Like

excuse me .what is scaleFactor exactly ?

The scaleFactor is used to adjust the repetition of a texture’s scale, especially when a mesh (like a wall or floor) is far from the camera. By scaling the texture up or down with the scaleFactor, it helps maintain a realistic appearance.

is this a realistic render in three.js? if yes can you share tips?

I’m not sure about it since I’m a beginner with Three.js.