Preserving UVs while modifying vertices

Hello everyone,

I have to admit it… I am too dumb to solve this puzzle on my own lol. This topic is on my list for a long time and I got already help from this community :slight_smile:. Until now I wasn’t 100% sure what I want to achieve and how I want to be implemented though. My comprehension regarding 3D increased since my initial attempt so I was able to dig a little bit further. I spent the last days to think about a method that will work and I tried countless approaches. Unfortunately I am missing some inevitable things to achieve the desired result.

I want to be able to adjust the position of specific vertices depending on the situation while preserving the UVs to avoid stretched or distorted textures on the mesh. I really want to modify the UV coordinates. I don’t want to use a shader, node material, etc. The mesh should use in its unmodified state the exact UV map I created before. While modifying some vertices the UV map should still be present but the affected UV coordinates should adjust accordingly.

3DS Max as well as Maya have an option called “Preserve UVs”. This is exactly what I try to do. Here are some videos that show it very well:

In my opinion the following things have to be done:

  1. Determining the delta vertices’ positions. Which vertex moves in which direction for which distance?
  2. To which extent is an affected triangle rotated in UV space.
  3. What is the current scale of the triangle in UV space?

I use right handed system and GLTF models. For each model I define some vertex groups (Blender) and export add these indices to my GLTF during export. So I am able to modify the right vertices easily.

For the beginning I am working with a simple cube. If this won’t work every other model will fail as well. Later the function should also handle some more complex models.

1. Scale vertex positions
For scaling the mesh I create 6 vertex groups (left, right, front, back, bottom, top), save the indices to my gltf and move them a specific distance in the given direction. No problem here.

const increase = vertexGroups[increaseVertexGroup].includes(i)
const decrease = vertexGroups[decreaseVertexGroup].includes(i)
if (increase || decrease) positionsTmp[pi] += distance * (decrease ? -1 : 1)

2. Determine which UVs have to be adjusted
I grab the normal of each face and sort them to know which UV coordinates are not affected. If I scale on x axis I dont have to adjust the UV coordinates of the faces with a normal of (0, 0, 1) for example.

const n = ["x", "y", "z"].sort((a, b) => Math.abs(normal[a]) - Math.abs(normal[b]))
if (n.includes(axis)) ...

3. Determine the rotation of the UV triangles
I tried multiple approaches here but it is still not working very well. I don’t get how to create a stable function that returns the rotation of a UV triangle. My idea was to grab two indices of a triangle and then compare the angle between both points in UV space

const uvV = uv2.subtract(uv1).normalize().multiplyByFloats(1, -1, 1)

and both positions in 3D space

const pV = p2.subtract(p1).normalize()

. Obviously I can’t compare a vector2 with a vector3. So I convert the vector3 to a vector2 by using a rotation matrix to get the face on a xy plane.

const quaternion = getQuaternion(normal, new BABYLON.Vector3(0, 0, 1))
const rpV = rotateByQuaternion(pV, quaternion)

const getQuaternion = (normal, plane) => {
    normal = normal.negate().normalize()

    let axis = BABYLON.Vector3.Cross(plane, normal)
    axis = axis.length() !== 0 ? axis : normal.z < 0 ? new BABYLON.Vector3(0, 2, 0) : new BABYLON.Vector3.Zero()

    const r = Math.acos(BABYLON.Vector3.Dot(normal, axis) / (normal.length() * axis.length())) || 0
    const rAxis = axis.multiplyByFloats(r, r, r)
    const rX = rAxis.x
    const rY = rAxis.y
    const rZ = rAxis.z

    const rotateX = BABYLON.Matrix.FromArray([
        1, 0, 0, 0,
        0, Math.cos(rX), -Math.sin(rX), 0,
        0, Math.sin(rX), Math.cos(rX), 0,
        0, 0, 0, 1

    const rotateY = BABYLON.Matrix.FromArray([
        Math.cos(rY), 0, Math.sin(rY), 0,
        0, 1, 0, 0,
        -Math.sin(rY), 0, Math.cos(rY), 0,
        0, 0, 0, 1

    const rotateZ = BABYLON.Matrix.FromArray([
        Math.cos(rZ), -Math.sin(rZ), 0, 0,
        Math.sin(rZ), Math.cos(rZ), 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1

    const rMatrix = rotateZ.multiply(rotateY).multiply(rotateX)

    return new BABYLON.Quaternion.FromRotationMatrix(rMatrix)

After this I can get the angle of each vector and subtract them to see the difference. I assume that this is the rotation angle of the UV coordinates.

const rUv = Math.atan2(uvV.y, uvV.x)
const rP = Math.atan2(rpV.y, rpV.x)
let r = rP - rUv

4. Determining the scale of the UV triangles
Since I know the rotation angle in UV space of the given triangle I had the idea to check which UV coordinates have an equal angle. Then I could calculate the distance between both points and use this length for further calculations.

const i1 = indices[faceIndex*3]
const i2 = indices[faceIndex*3+1]
const i3 = indices[faceIndex*3+2]

const uv1 = new BABYLON.Vector3(uvs[i1*2], uvs[i1*2+1], 0)
const uv2 = new BABYLON.Vector3(uvs[i2*2], uvs[i2*2+1], 0)
const uv3 = new BABYLON.Vector3(uvs[i3*2], uvs[i3*2+1], 0)

const r1 = getAngleBetweenVectors(uv1, uv2, true)
const r2 = getAngleBetweenVectors(uv1, uv3, true)
const r3 = getAngleBetweenVectors(uv2, uv3, true)

let l
if (compareNumbers(r1, rotation, 1)) l = BABYLON.Vector3.Distance(uv1, uv2)
else if (compareNumbers(r2, rotation, 1)) l = BABYLON.Vector3.Distance(uv1, uv3)
else if (compareNumbers(r3, rotation, 1)) l = BABYLON.Vector3.Distance(uv2, uv3)

let d = l * (ratio - 1)

return d/2

const getAngleBetweenVectors = (v1, v2, clean) => {   
    const d = v2.subtract(v1)
    let r = Math.atan2(d.y, d.x)
    if (r === Math.PI) r = 0

    if (clean) {
        r = Math.abs(r)
        if (r > Math.PI/2) r = Math.PI % r
    return r

const compareNumbers = (n1, n2, decimals, absolute) => {
    if (absolute) {
        n1 = Math.abs(n1)
        n2 = Math.abs(n2)

    return n1.toFixed(decimals) === n2.toFixed(decimals)

5. Setting new UV coordinates
Knowing the rotation angle and the necessary distance I can adjust the UV coordinates in the same way I modify the vertex positions.

const n = normal.negate().normalize()
let add = new BABYLON.Vector3(distance * Math.cos(rotation), distance * Math.sin(rotation), 0)

uvsTmp[index*2] += add.x
uvsTmp[index*2+1] += add.y

Unfortunately this code only works for a simple cube and by using some hacks for getRotation() and setVertexUv(). I can’t recognize a pattern to make it work without the hacks.

Anyway you can see it in action here:

So how am I able to do it right? How to calculate the rotation angle? How do I scale the UV coordinates correctly? How am I able to handle more complex models than a cube?

If anyone is able to help me here I would be really grateful. Since I want this function for a long time I am thinking about posting this topic to the job section. But I wanted to try it here first.

Thank you!