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):
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.
I’ve created a diagram showing what’s happening vs. what should happen:
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?
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);
Vector3 displacement = closestPoint - worldPos;
Vector3 displacementOnPlane = displacement - Vector3.Dot(displacement, worldNormal) * worldNormal;
Vector3 targetPosition = worldPos + displacementOnPlane * deformationStrength;
targetPosition += worldNormal * height;
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.
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;