How to texture a groundmesh thats been displaced by a heightmap

So What im trying to do is, displace a normal ground mesh, to create realistic terrain and then create a physics body in that same shape in rapier (This parts working), after apply textures to the visual mesh

But what ends up happening is if i set the diffues or normal map of the following textures to repeat 5 or something different, it distorts the visual mesh and the physics mesh stays the same as diaplced

I tried cloneing the ground mesh then applying, but same issue. Seems the issue is the displacement is done by the shader in three js, so its basically dynamic? and keeps changing based on new data

Code here:

// Function to create the ground mesh using the heightmap
function createGround(world) {
    // Hardcoded values
    const widthSeg = 100;    // Number of segments along width
    const heightSeg = 100;   // Number of segments along height
    const horTexture = 1;    // Horizontal texture repeat
    const verTexture = 1;    // Vertical texture repeat
    const dispScale = 90;    // Displacement scale

    // Create plane geometry
    const groundGeo = new THREE.PlaneGeometry(1000, 1000, widthSeg, heightSeg);

    // Create material (without displacement map set yet)
    const groundMat = new THREE.MeshStandardMaterial({
        color: 0xe00e00,           // Red color
        wireframe: false,           // Wireframe mode
        displacementScale: dispScale // Displacement intensity
    });

    // Create and configure the ground mesh
    const groundMesh = new THREE.Mesh(groundGeo, groundMat);
    scene.add(groundMesh);
    groundMesh.rotation.x = -Math.PI / 2; // Rotate to lie flat
    groundMesh.position.y = -0.5;         // Shift slightly below origin

    // Load heightmap texture asynchronously
    new THREE.TextureLoader().setPath("assets/textures/").load("heightmap4.jpg", (dispMap) => {
        // Configure texture wrapping and repeat
        dispMap.wrapS = dispMap.wrapT = THREE.RepeatWrapping;
        dispMap.repeat.set(horTexture, verTexture);

        // Assign displacement map to material and update
        groundMat.displacementMap = dispMap;
        groundMat.needsUpdate = true;

        // Extract height data from the texture
        const canvas = document.createElement("canvas");
        canvas.width = dispMap.image.width;
        canvas.height = dispMap.image.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(dispMap.image, 0, 0);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const pixels = imageData.data;

        // Number of vertices is segments + 1
        const nrows = heightSeg + 1; // 101 rows
        const ncolumns = widthSeg + 1; // 101 columns
        const heights = new Float32Array(nrows * ncolumns);

        // Sample texture at each vertex’s UV coordinates
        for (let j = 0; j < nrows; j++) {
            for (let i = 0; i < ncolumns; i++) {
                const u = i / widthSeg; // UV from 0 to 1
                const v = j / heightSeg; // UV from 0 to 1
               // const texX = Math.floor(u * dispMap.image.width);
               // const texY = Math.floor(v * dispMap.image.height);
                const texX = Math.min(Math.floor(u * dispMap.image.width), dispMap.image.width - 1);
                const texY = Math.min(Math.floor(v * dispMap.image.height), dispMap.image.height - 1);
                const index = (texY * dispMap.image.width + texX) * 4; // RGBA, so *4
                const heightValue = pixels[index] / 255; // Red channel (0-1), assuming grayscale
                heights[j * ncolumns + i] = heightValue * dispScale; // Scale to match Three.js
            }
        }

//working basic plane
//const planeColliderDesc = RAPIER.ColliderDesc.cuboid(1000, 0.1, 1000);
//world.createCollider(planeColliderDesc, terrainRigidBody);
//console.log("Plane collider created, debug vertices:", world.debugRender().vertices.length);

// Create the heightfield collider
// Rapier expects a different structure for the scale
const halfExtentsX = 500; // half of your 1000 size
const halfExtentsZ = 500; // half of your 1000 size

// 1. Create static rigid body
const terrainRigidBodyDesc = RAPIER.RigidBodyDesc.newStatic()
    .setTranslation(0, -0.5, 0); // Match your mesh position
const terrainRigidBody = world.createRigidBody(terrainRigidBodyDesc);

const transformedHeights = new Float32Array(heights.length);
for (let j = 0; j < nrows; j++) {
    for (let i = 0; i < ncolumns; i++) {
      // 90° clockwise + horizontal flip
      // ends up as: newRow = i, newCol = j
      transformedHeights[i * nrows + j] = heights[j * ncolumns + i];
    }
  }
  
  
  

// Create the heightfield collider - note the correct parameter order
const terrainColliderDesc = RAPIER.ColliderDesc.heightfield(
    nrows - 1,
    ncolumns - 1,
    transformedHeights, // Use transformed array
    { x: 1000, y: 1, z: 1000 } // Use normalized scale (y=1) and apply height scale later
);

//terrainColliderDesc.setScale(1.0, dispScale, 1.0);
// Create the collider
world.createCollider(terrainColliderDesc, terrainRigidBody);

// Define the scale for the heightfield
const scale = { x: 1000, y: 1000 };

// Create the heightfield collider
//const terrainColliderDesc = RAPIER.ColliderDesc.heightfield(nrows, ncolumns, heights, scale);
//world.createCollider(terrainColliderDesc, terrainRigidBody);



    });
}

This is without the texture adding part as I don’t know how best to apply it without breaking the visual height of the mesh

Yes you guessed right. Shaders only change what you see on the GPU side (on rendering step) meanwhile physics engine operate before it happen at the geometry level.

If you want to add physics there is two options:
-not using shaders to displace, changing vertex positionAttribute, and work with an engine like Rapier sending your geometry to it.
-use shaders and integrate physics into it. without an engine. (this is expert level stuff but not totally impossible with webGPU + TSL)

3 Likes

Awesome. That worked option 1

Infact seems to be more accurate than the shader one, i didn’t check the exact values the first time but was getting some morhping through the terrain due to mismatch of physics and mesh/render

1 Like