Search code examples
c#unity-game-enginemesh

How to modify a 3D mesh to follow terrain while preserving thickness?


For a Unity project, I need to create a script that modifies the mesh of a 3D object during runtime so that it follows the shape of the terrain underneath it.

Currently, I’ve achieved this result (before/after):

Before After

The model correctly follows the curve of the ground, but the thickness is not preserved at all. The model becomes very thin, and all details are lost.

before after

I’ve created a diagram showing what’s happening vs. what should happen:

graph

Here’s the current code:

public LayerMask groundLayer;
public float maxRaycastDistance = 100f;
public float deformationStrength = 1.0f;
private MeshFilter meshFilter;
private Mesh originalMeshe;
private Mesh workingMeshe;
private Vector3[] originalVertice;
private Vector3[] deformedVertice;
public float height = 3f;

private void InitializeMeshe()
{
    meshFilter = GetComponent<MeshFilter>();
    var meshRenderers = GetComponent<MeshRenderer>();

    height = meshRenderers.bounds.size.y;

    originalMeshe = meshFilter.sharedMesh;
    workingMeshe = Instantiate(originalMeshe);
    originalVertice = workingMeshe.vertices;
    deformedVertice = new Vector3[originalVertice.Length];

    meshFilter.mesh = workingMeshe;
}

public void DeformMeshe()
{
    Vector3[] deformedVerts = new Vector3[originalVertice.Length];

    for (int j = 0; j < originalVertice.Length; j++)
    {
        Vector3 worldPos = meshFilter.transform.TransformPoint(originalVertice[j]);
        Vector3 closestPoint = worldPos;
        Vector3? groundHeight = GetGroundHeight(worldPos);

        if (groundHeight == null)
        {
            deformedVerts[j] = originalVertice[j];
            continue;
        }

        float closestDistance = Vector3.Distance(worldPos, groundHeight.Value);
        if (closestDistance < maxRaycastDistance) closestPoint = groundHeight.Value;

        Vector3 targetPosition = Vector3.Lerp(worldPos, closestPoint, deformationStrength);
        targetPosition.y += height;

        deformedVerts[j] = meshFilter.transform.InverseTransformPoint(targetPosition);
    }

    workingMeshe.vertices = deformedVerts;
    workingMeshe.RecalculateNormals();
    workingMeshe.RecalculateBounds();
}

public Vector3? GetGroundHeight(Vector3 worldPos)
{
    if (Physics.Raycast(worldPos, Vector3.down, out RaycastHit hitDown, maxRaycastDistance, groundLayer))
        return hitDown.point;

    if (Physics.Raycast(worldPos, Vector3.up, out RaycastHit hitUp, maxRaycastDistance, groundLayer))
        return hitUp.point;

    return null;
}

How can I fix my code to achieve my goal?

  1. The initial technique involves casting raycasts toward the ground to determine its height and adjusting the Y position of the vertices accordingly.
  2. I tried using the Normals to maintain a consistent distance between the points.
Vector3 normalWorld = meshFilter.transform.TransformDirection(vertexNormals[j]);
float originalDistance = Vector3.Dot(originalVertice[j], vertexNormals[j]);
Vector3 adjustedPosition = closestPoint + normalWorld * originalDistance;

Vector3 targetPosition = Vector3.Lerp(worldPos, adjustedPosition, deformationStrength);

Result with Normals

  1. Still using the Normals, I attempted to move the points along a plane. The result is identical to attempt 2.
Vector3 displacement = closestPoint - worldPos;
Vector3 displacementOnPlane = displacement - Vector3.Dot(displacement, worldNormal) * worldNormal;
Vector3 targetPosition = worldPos + displacementOnPlane * deformationStrength;
targetPosition += worldNormal * height;

Solution

  • It seems that you only need to move the vertices vertically. You need a horizontal reference plane, and the distance that each vertex on the mesh needs to move is based on the vertical distance from its corresponding position on the reference plane to the terrain mesh.

    diagram

    • red: reference plane
    • blue: terrain
    • black: mesh
    • gray: deformed mesh
    var p = worldPos;
    p.y = REF_Y; // The position of the reference plane
    float closestDistance = Vector3.Distance(p, groundHeight.Value);
    
    var targetPosition = worldPos;
    targetPosition.y -= closestDistance * deformationStrength;
    targetPosition.y += height;