Manually provide a UV map to the PlaneGeometry

Hi, here’s a basic code I wrote for some test rendering:

import * as THREE from "three";
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { useRenderer } from "./useRenderer";

const normalizeUvCoordinate = (coordinate: number, range: number) => {
    return coordinate / range;
}

/**
 * Renders a terrain with tiled textures based on a heightmap and terrainMap.
 */
export const renderTerrainWithTextures = async (
    container: HTMLElement,
    terrainTextures: string[]
) => {
    const width = 64;
    const height = 64;
    const textureWidth = 32;
    const textureHeight = 32;

    const scene = new THREE.Scene();
    const aspect = container.offsetWidth / container.offsetHeight;
    const camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000);
    
    // Load terrain textures
    const textureLoader = new THREE.TextureLoader();
    const loadedTextures: { [key: number]: THREE.Texture } = {};
    for (let terrainType = 0, l = terrainTextures.length; terrainType < l; terrainType++) {
        const textureUrl = terrainTextures[terrainType];
        loadedTextures[Number(terrainType)] = await textureLoader.loadAsync(textureUrl);
    }

    // Create terrain geometry
    const geometry = new THREE.PlaneGeometry(width, height, width - 1, height - 1);

    // Creating uv
    const uvLength = width * height * 2;
    const uvArray = new Float32Array(uvLength);
    for (let i = 0; i < uvLength; i = i + 2) {
        const indexW = (i / 2) % width;
        const indexH = Math.floor((i / 2) / width);
        uvArray[i] = normalizeUvCoordinate(indexW % textureWidth, textureWidth);
        uvArray[i + 1] = 1 - normalizeUvCoordinate(indexH % textureHeight, textureHeight);
    }

    geometry.setAttribute("uv", new THREE.BufferAttribute(uvArray, 2))
    
    const material = new THREE.MeshBasicMaterial({
        map: loadedTextures[0],
        side: THREE.DoubleSide,
    });
    const test = new THREE.Mesh(geometry, material);
    scene.add(test);
    
    const renderer = useRenderer(container, scene, camera);

    const controls = new OrbitControls( camera, renderer.domElement );
    controls.update();
};

The reason I want to provide a uv array manually is because later the mapping is gonna be even more complicated, rigth now I just want my 32x32 texture be repeated 4 times on 64x64 plane. The math looks right, but instead of just repeating the texture I get a tiny “A” in the middle of the plane and some stretched “A” on the main axis (attached). Can someone, please, take a look on that and say what’s wrong with the map?

test

texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
The default is ClampToEdgeWrapping, which I think is what you are describing.
Also, if you’re just trying to repeat… you can use texture.repeat.set(4,4)
to do it via the texture instead of modifying UVs.
There is also texture.rotation

This one did not work as well.
The problem with repeat is that later I would like to achieve more complicated mapping. For example, transform texture like a radio wave, but only in an area where some object is located, so I definitely need to set uv mapping point by point