tsStickyLips tool

This tool will let you easily create a sticky lips setup for your character. The setup created is based on a wire deformer.

Installation

1. Download the script from this link: tsStickyLips.rar;

2. Copy file tsStickyLips.pyc into your maya scripts/ folder;

3. From the script editor, in a python tab type:

import tsStickyLips

tsStickyLips.main()

Usage

As the sticky lips deformation lies on top of other deformations, this should be your very last step in facial rigging.

1. Select top edges for the uplip and press ‘<<<’;

2.  Select top edges for the bottomlip and press ‘<<<’;

3. Press ‘Create stickyLips’ button.

The script will create a copy of the geometry with sticky lips setup. You can hide the previous geo and go rendering with this copy.

 

Optional parameters:

The tool provides automatically on weighting the wire deformer influence areas. Two optional parameters can be tweaked to change how the weighting is done:

- Selection growth: Grows vertex selection before setting weight influence to 1;

- Smoothness: How many times the weighting should be smoothed.

 

Here’s the source code of the main procedure for the tech guys:

def createStyckyLips(top_edges, bottom_edges, selection_growth = 2, smoothing=8):
 base_mesh = top_edges[0].split(".")[0]
 cmds.select(base_mesh)

 # Create curve on top edge sel on duplicated mesh
 cmds.select(top_edges)
 top_curve = cmds.polyToCurve(form=2, degree=1)[0]
 top_curve_shape = cmds.listRelatives(top_curve, c=1)[0]

 # Create curve on bottom edge sel on duplicated mesh
 cmds.select(bottom_edges)
 bottom_curve = cmds.polyToCurve(form=2, degree=1)[0]
 bottom_curve_shape = cmds.listRelatives(bottom_curve, c=1)[0]

 # Create wire average curve
 avg_node = cmds.createNode("avgCurves")
 cmds.setAttr(avg_node + ".automaticWeight", 0)
 cmds.setAttr(avg_node + ".automaticWeight", 0)
 avg_curve = cmds.duplicate(top_curve)[0]
 avg_curve_shape = cmds.listRelatives(avg_curve, c=1)[0]
 cmds.connectAttr(top_curve_shape + ".worldSpace[0]", avg_node + ".inputCurve1", force=1)
 cmds.connectAttr(bottom_curve_shape + ".worldSpace[0]", avg_node + ".inputCurve2", force=1)
 cmds.connectAttr(avg_node + ".outputCurve", avg_curve_shape + ".create", force=1)

 #Create duplicate mesh with inputs
 cmds.select(base_mesh)
 dup_mesh = mel.eval("polyDuplicateAndConnect;")[0]
 dup_top_edges = [x.replace(base_mesh, dup_mesh) for x in top_edges]
 dup_bottom_edges = [x.replace(base_mesh, dup_mesh) for x in bottom_edges]

 # Create wire deformer on duplicate mesh
 wire_deformer = mel.eval("wire -gw false -en 1.000000 -ce 0.000000 -li 0.000000 -w " + avg_curve + " " + dup_mesh +";")[0]
 base_wire = avg_curve + "BaseWire"
 base_wire_shape = cmds.listRelatives(base_wire, c=1)[0]
 cmds.connectAttr(avg_node + ".outputCurve", base_wire_shape + ".create", force=1)

 # Set wire deformer params
 cmds.setAttr( wire_deformer + ".scale[0]", 0)
 cmds.setAttr( wire_deformer + ".envelope", 1.2)
 editAttrs(wire_deformer, ["ce","te","li","ro","sc[0]"], l=1, k=0, cb=0)

 #Set weights
 cmds.select(dup_mesh)

 #Zero all weights
 mel.eval("artAttrToolScript 4 \"" + wire_deformer +"\";")
 mel.eval("artAttrPaintOperation artAttrCtx Replace;")
 mel.eval("artAttrCtx -e -value 0 `currentCtx`;")
 mel.eval("artAttrCtx -e -clear `currentCtx`;")

 #Select vertices
 cmds.select(dup_mesh)
 mel.eval("changeSelectMode -component;")
 mel.eval("hilite -r " + dup_mesh + " ;")
 mel.eval("setComponentPickMask \"Line\" true;")
 cmds.select(dup_top_edges, dup_bottom_edges)
 mel.eval("ConvertSelectionToVertices;")

 for i in range(selection_growth):
 mel.eval("GrowPolygonSelectionRegion;")

 #Set weights for region to 1
 mel.eval("artAttrInitPaintableAttr;")
 mel.eval("artAttrValues artAttrContext;")
 mel.eval("toolPropertyShow;")

 mel.eval("artAttrToolScript 4 \"" + wire_deformer +"\";")
 mel.eval("artAttrPaintOperation artAttrCtx Replace;")
 mel.eval("artAttrCtx -e -value 1 `currentCtx`;")
 mel.eval("artAttrCtx -e -clear `currentCtx`;")

 #Smooth n times
 cmds.select(dup_mesh)
 mel.eval("artAttrInitPaintableAttr;")
 mel.eval("artAttrValues artAttrContext;")
 mel.eval("toolPropertyShow;")
 mel.eval("artAttrPaintOperation artAttrCtx Smooth;")

 for i in range(smoothing):
 mel.eval("artAttrCtx -e -clear `currentCtx`;")

 mel.eval("changeSelectMode -object;")
 cmds.select(cl=1)

 # Group,rename all objects
 main_grp = cmds.group(em=1, w=1, n=resolveName("GRP_sticky"))
 cmds.setAttr(main_grp + ".visibility", 0)
 editAttrs(main_grp, ["tx","ty","tz","rx","ry","rz","sx","sy","sz"], l=1, k=0, cb=0)

 top_curve = cmds.rename(top_curve, resolveName("CRV_sticky_top"))
 bottom_curve = cmds.rename(bottom_curve, resolveName("CRV_sticky_bottom"))
 avg_curve = cmds.rename(avg_curve, resolveName("CRV_wire"))
 base_wire = cmds.rename(base_wire, resolveName(avg_curve + "BaseWire"))

 cmds.parent(top_curve, bottom_curve, avg_curve, base_wire, main_grp)
 if cmds.listRelatives(dup_mesh, p=1):
 cmds.parent(dup_mesh, w=1)

 wire_deformer = cmds.rename(wire_deformer, resolveName("stickyWire"))
 cmds.select(dup_mesh)

Have fun!

 

 

tsFrameBlendshapes tool

This tool is part of my personal toolset.
It let’s you easily create, connect and animate animation based corrective blendshapes,.
Just go to the frame you want to correct/deform, hit “Sculpt” and edit the shape of your mesh. The cool thing is that the script works on pose: you can sculpt the corrective shape in any pose, then the script will bring back your deformed mesh in base pose automatically to create your corrective shape.
Coded in python.

Instancing random objects preserving frequency

Hi all. After the post talking about the math involving frequency for procedural animation, it is now time to talk about the randomness.

During the production in studio we had to deal with the following question: instantiate some base objects along particles randomly to form a crowd, using the Maya Instancer, but having the control over the count of the instances for each object (to have some characters less frequent than others).

The Gaussian approach

The most natural attempt would be using a Gaussian distribution (or Normal distribution), which results in a non-uniform distribution of the samples, being more dense around the mean value, having the variance as measure of the width of  distribution.

Following the Gaussian approach, we can use the result of a random.gauss() function as the index of the element extracted from the list of our objects to instance:


import random

numObjs =10
numSamples=600
dev=2

random.seed(2233)
l= range(numObjs)
count = [0 for x in l]

for i in range(numSamples-1):
    while True:        
        v = int (abs(random.gauss(0,dev)))
        if v < len(l):
            count[v] +=1
            break
print count
        

The list “l” contains the objects we want to instance (for the sake of simplicity, I filled it out with numbers as [0, 1, .... numObjs] ), wile numSamples reprents the numbers of object we want to instance (i.e. the number of extractions).
To get stable results, I initialized the seed of the random function (!Important).
What we are doing in the two nested loops is extracting #numSamples times a random value from the Gaussian distribution, having as mean value 0 (the bell centered at origin) and dev as the width of the bell curve. Since we are using the result of the extraction as index for the list containing our elements, the result must be an integer value and equal or greater than zero. Because our list has fixed size (equal to len(l) ) and the bell goes from -infinite to infinite, we repeat the estraction in case the resulting value is out of the list indices range.
The code above produce the following count:

[209, 175, 112, 61, 37, 3, 2, 0, 0, 0]

As we can see, the values on the left are extracted more frequently than the one on the right.
Because we are instancing objects, it would be nice to get some instances for each object.
To achieve this, we can enlarge the width of the bell curve by increasing dev:

dev=3: [141, 126, 117, 82, 53, 38, 27, 12, 1, 2]

dev=5: [92, 94, 78, 86, 68, 64, 36, 35, 25, 21]

dev=10: [67, 69, 63, 65, 67, 52, 61, 59, 43, 53]

As the numbers above suggests, the higher the deviation, the more the values tend to be uniform. By playing with the dev value, we have a certain amount of freedom on driving the extraction frequency of each sample, thus the frequency of instancing of each object.
Even if this approach is mathematically correct, it lacks on control over the real count (you can try to change the seed and you will easily get different values) plus it gives unpredictable results when the population (the number of base object) is numerically comparable to the number of extractions (the object to instance).
For example, let’s see this case:

numObjs= 20, numSamples=30, dev=2:

[13, 7, 4, 4, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

We see that the bell is not large enough to give at least one instance to the last elements. Let’s try to increase dev:

dev=10: [3, 6, 1, 2, 1, 2, 1, 0, 2, 2, 2, 1, 1, 0, 0, 0, 0, 1, 1, 3]

Now we get a more even distribution, but we see some 0s around: that means some objects will be never instanced and we can even see that the higher number is the second (while we expect to be the first one): that means the element we want to be more frequent is less frequent than the second one, by changing the seed, we could be more lucky, but for sure we don’t have full control of what’s going on.

The Modulo approach

Let’s follow another approach, using the Modulo operation.
If we want evenly distribute some objects extracted from a list, we can exctract the element at index:

index = #numObjs mod n

Where n represents the number of the current extraction:


import random

l = ["Obj1", "Obj2", "Obj3", "Obj4", "Obj5"]
count = [0,0,0,0,0]

numInstances = 10

for i in range(numInstances):
    index = i %len(l)
   
    #instance object l[index]
    #...
   
    print l[index]
    count[index] +=1

print count

This code produces the following extractions: Obj1, Obj2, .., Obj5, Obj1, Obj2, .. and obviously the count would be equal for all objects (where the number of extractions is a multiple of the number of elements).
To get a random and numerically stable extraction, we can shuffle our list:


import random

random.seed(10)
l = ["Obj1", "Obj2", "Obj3", "Obj4", "Obj5"]
random.shuffle(l)
count = [0,0,0,0,0]

numInstances = 10

for i in range(numInstances):
    index = i %len(l)
   
    #instance object l[index]
    #...
   
    print l[index]
    count[index] +=1

The last step is adding the information about the frequency.
Let’s set the frequency for each element and rebuild the list l by adding each element # times as much as its frequency. For instance:

f['Obj1'] = 3
f['Obj2'] = 1
f['Obj3'] = 2

Our list would be: l=['Obj1', 'Obj1', 'Obj1', 'Obj2', 'Obj3', 'Obj3']
Now by shuffling this list, we can use the modulo function for the estraction, preserving the frequencies set above:


import random

random.seed(10)

f=dict()

f["Obj1"] = 3
f["Obj2"] = 1
f["Obj3"] = 2

l = list()

for k, v in f.items():
    for i in range(v):
        l.append(k)

print "Base list: ", l

random.shuffle(l)

print "Shuffled list: ", l

count = dict()

count["Obj1"] = 0
count["Obj2"] = 0
count["Obj3"] = 0


numInstances = 6

for i in range(numInstances):
    index = i %len(l)
    
    #instance object l[index]
    #...
    
    count[l[index]] +=1

print "Objects count: ", count

The above code produces the following output:

Base list: ['Obj1', 'Obj1', 'Obj1', 'Obj3', 'Obj3', 'Obj2']
Shuffled list: ['Obj2', 'Obj1', 'Obj1', 'Obj3', 'Obj1', 'Obj3']
Objects count: {‘Obj1′: 3, ‘Obj3′: 2, ‘Obj2′: 1}

Let’s raise the number of instances (numInstances=600):

Objects count: {‘Obj1′: 300, ‘Obj3′: 200, ‘Obj2′: 100}

We can see that the frequency is preserved. Everything works so far, except one last thing: we are getting a pseudo-random extractions that repeats the sequence everytime we visit the whole list l.
To get a more random behaviour, we can shuffle the list everytime we restart by extracting the first element, i.e.: index == 0:


import random

random.seed(10)

f=dict()

f["Obj1"] = 3
f["Obj2"] = 1
f["Obj3"] = 2

l = list()

for k, v in f.items():
    for i in range(v):
        l.append(k)

print "Base list: ", l

random.shuffle(l)

print "Shuffled list: ", l

count = dict()

count["Obj1"] = 0
count["Obj2"] = 0
count["Obj3"] = 0


numInstances = 12

for i in range(numInstances):
    index = i %len(l)
    if index == 0:
        random.seed(i)
        random.shuffle(l)
        
    #instance object l[index]
    #...
    print l[index]
    count[l[index]] +=1

print "Objects count: ", count

And don’t forget to change your seed before the shuffle() operation, or you’ll get the same list over and over.
The code above produces the following sequence:

Obj1, Obj1, Obj2, Obj1, Obj3, Obj3, Obj1, Obj2, Obj1, Obj1, Obj3, Obj3

With Objects count: {‘Obj1′: 6, ‘Obj3′: 4, ‘Obj2′: 2}.

Finally we got a way to instantiate objects randomly but preserving control over their frequency.
I suggest to use the Gaussian approach only in case our base population is not numerically comparable to the number of instances.

keyBaker tool

Download it here: Key Baker

Purpose:
This plugin bakes the animation on the selected objects and save the bake on a file.
You can then import and apply the baked animation just by browsing the bake file in the Import tab.

The export interface


The import interface

How it works:

For each selected object, the script saves the values of each published and keyable attribute for each frame in the selected interval.
When you apply the cache, the script simply looks for each object in the bake and keys the relative attributes to the values saved in the bake.

Be careful that applying the animation bake will destroy any keyframed animation currently set to your objects. Thus, the right way to go would be caching the animation and then applying the cache to a clean copy of the scene, with no keys, expressions or constraints that might control the published attributes.

Installation:
Copy the file keyBaker.pyc into your script folder, source the file and call the main() function, as shown here:


import keyBaker
keyBaker.main()

For any suggestion, bug report or question, you are free to contact me.

OBJ flow exporter

This tool allows to export and import a mesh as a flow of OBJ files, driven by an expression, written in python.
It is based on the standard Maya exporter plugin.

Watch the preview here:

It is a bit raw, but it does its work pretty well. We used it in production for managing a metaball lava flow and it worked very fast.

Installation

Copy the .py file into your Maya scripts folder;

Import the script;

Call these functions:

displayExportUI(): for the exporting tool;

displayImportUI(): for the importing tool.

After the job is done, you can tweak the animation of your OBJ flow just by editing the expression created by the script.

You can download it here: bakeToObj.py.zip

Feel free to edit and improve the code and I’d like to keep updated for any improvement.

Please note that once you import an obj flow, you have effectively separate meshes, that means you can’t for example use motion blur, since at each frame the visible mesh changes.

Have fun!

Function createFollicle()

This function I wrote in python is very useful to create Follicles on surfaces and polygons, without the need of creating Hairs and then deleting the Hair System and the curves just to keep the follicles.
Plus, of course, is a good chance to code something and to have a function you can use in your scripts.

How does it works..

The function requires as parameters a list of 3 elements, named pos for a point position in 3d space and a nurbs surface OR a poly surface.

The follicle is located on the polygon/surface by finding the closest point on it from the given position.

I have included a second function, named createFollicles(), where you can pass an array of 3d points instead just one point. It will create multiple follicles.

Here’s a quick example..

I create a torus polygon, select some vertices, get their positions and create follicles on the vertices..


torus = cmds.polyTorus()
cmds.select(torus[0]+".vtx[10]", torus[0]+".vtx[20]", torus[0]+".vtx[30]")
sel = cmds.ls(sl=1, fl=1)

for obj in sel:
	pos = cmds.pointPosition(obj, w=1)
	follicle = createFollicle(pos, poly_surface = torus[0])
	print follicle

Here’s the function code:

import maya.cmds as cmds
import maya.OpenMaya as OpenMaya

def createFollicle (pos=[0, 0, 0], nurbs_surface=None, poly_surface=None):
	
	if (nurbs_surface==None and poly_surface==None):
		OpenMaya.displayError("Function createFollicle() needs a nurbs surface or poly surface")
		return

	transform_node = cmds.createNode("transform")
	cmds.setAttr((transform_node +".tx"), pos[0])
	cmds.setAttr((transform_node +".ty"), pos[1])
	cmds.setAttr((transform_node +".tz"), pos[2])
	
	#make vector product nodes to get correct rotation of the transform node
	vector_product = cmds.createNode("vectorProduct")
	cmds.setAttr((vector_product+".operation"), 4)
	cmds.connectAttr( (transform_node+".worldMatrix"), (vector_product+".matrix"), f=1)
	cmds.connectAttr( (transform_node+".rotatePivot"), (vector_product+".input1"), f=1)

	#connect the correct position to a closest point on surface node created
	if nurbs_surface:
		closest_position = cmds.createNode("closestPointOnSurface", n=(transform_node+"_CPOS"))
		cmds.connectAttr( (nurbs_surface+".ws"), (closest_position+".is"), f=1)
		cmds.connectAttr( (vector_product+".output"), (closest_position+".inPosition"), f=1)
	
	if poly_surface:
		closest_position = cmds.createNode("closestPointOnMesh", n=(transform_node+"_CPOS"))
		cmds.connectAttr( (poly_surface+".outMesh"), (closest_position+".im"), f=1)
		cmds.connectAttr( (vector_product+".output"), (closest_position+".inPosition"), f=1)

	#create a follicle node and connect it
	follicle_transform = cmds.createNode("transform", n=(transform_node+"follicle"))
	follicle = cmds.createNode("follicle", n=(transform_node+"follicleShape"), p=follicle_transform)
	cmds.connectAttr((follicle+".outTranslate"), (follicle_transform+".translate"), f=1)
	cmds.connectAttr((follicle+".outRotate"), (follicle_transform+".rotate"), f=1)
	if nurbs_surface:
		cmds.connectAttr((nurbs_surface+".local"), (follicle+".is"), f=1)
		cmds.connectAttr((nurbs_surface+".worldMatrix[0]"), (follicle+".inputWorldMatrix"), f=1)
	if poly_surface:
		cmds.connectAttr((poly_surface+".outMesh"), (follicle+".inm"), f=1)
		cmds.connectAttr((poly_surface+".worldMatrix[0]"), (follicle+".inputWorldMatrix"), f=1)

	cmds.setAttr((follicle+".parameterU"), cmds.getAttr (closest_position+".parameterU"))
	cmds.setAttr((follicle+".parameterV"), cmds.getAttr (closest_position+".parameterV"))

	#return strings
	cmds.delete(transform_node)
	return [follicle_transform, follicle, closest_position]
	
def createFollicles  (follicle_positions=[[0,0,0]], nurbs_surface=None, poly_surface=None):

	out_follicles=list()

	if (nurbs_surface==None and poly_surface==None):
		OpenMaya.displayError("Function createFollicles() needs a nurbs surface or poly surface")
		return
	 	
	for pos in follicle_positions:
		lst = createFollicle(pos, nurbs_surface, poly_surface)
		out_follicles.append(lst)
	return out_follicles

Function resolveName()

When you call this function, it gives you back a unique name in the Maya scene. Very useful when creating objects programmatically and you want to keep unique names in your scene (Essential when dealing with maya scripts).

If the string passed as parameter is not assigned yet, then the function returns the string itself, else it will return the string plus “_#”.

Python code:


def resolveName(n):
	name = n
	if cmds.objExists(n):
		i = 1
		while cmds.objExists(str(n) + "_" + str(i)):
			i+=1
		name = str(n) + "_" + str(i)
		print "Warning: Object named " + n + " already exists. Used " + name + " instead."
	return name

MEL code:

proc string resolveName(string $n){
	string $name = $n;
	if (`objExists $n`){
		int $i = 1;
		while (`objExists ($n + "_" + string($i))`){
			$i+=1;
		}
		$name = $n + "_" + string($i);
		print ("Warning: Object named " + $n + " already exists. Used " + $name + " instead.");
	}
	return $name;
}

Examples:

Suppose you want to create a series of objects using the same name. By calling the function before the name you don’t have to worry about naming conflicts:


import maya.cmds as cmds

num_spheres = 10
sphere_list = list()

for i in range(numspheres):
     new_sphere = cmds.sphere(n=resolveName("mySphere"))
     sphere_list.append(new_sphere)

This will create the spheres mySphere, mySphere_1, mySphere_2 and so on.
If you recall the function again, the numbering will start from the first available name.

Merging vertices at corner

Hi,

Today a Modeler at work asked to me if there is a tool in Maya to merge two vertices from 2 different meshes in the corner, (not just by averaging their positions) like depicted in figure:

I was unsure about the toolset for modeling provided by Maya, but I thought it was a good chance to write out some code and have fresh material for the blog.

My idea was to create a script that, by selecting some vertices or edges from different (or the same) meshes would move the external points right to the corner.

This very simple task presents some non-trivial challenges.

First of all, what we ‘see’ programmatically from a mesh is just a cloud of points, their connections (the edges), the faces and so on, so we have to figure what we need as input from the user and how to elaborate it to achieve the result.

When developing tools, one of the crucial aspect I focus more is keeping the tool usable, that means simple and easy to understand for the user.

Here’s my approach:

Imagine if we could extend the longitudinal edges for each mesh: the intersection points between these edges would give us the final positions where to place the vertices at the border for each mesh.

Mathematically speaking, our problem can be reduced into calculating the intersection points between 2 lines representing 2 adjacent edges from the different meshes.

Now we can remodel the problem in this way: given 2 edges, calculate their intersection and move the closer points to the intersection.

Given 2 lines R and S in a 3d space (in our case constructed by extending to infinite 2 mesh edges), we might have the following cases:

- R intersect S in P(x’, y’, z’);

- R || S => they are parallel, thus S never intersect R or R=S always;

- R and S are crooked: never intersect and no parallel.

According the different cases, we might approach different solutions. I opted for averaging the border vertices in case S=R and in the other cases finding the 2 closest points P and R where P in R and Q in S, so we keep the direction of the edge. In case R intersect S, it follows that P=R.

Selected 2 edges, we can find the two extreme vertices using the polyInfo command:


sel = cmds.ls(sl=1)
if len(sel) !=2:
   OpenMaya.MGlobal.displayError("Select exactly 2 edges")
   return

   vtxsA = cmds.polyInfo(sel[0], edgeToVertex=1)[0].split()
   vtxsB = cmds.polyInfo(sel[1], edgeToVertex=1)[0].split()

Now we have a set of four points. We need to know wich 2 of these points to move after we find the intersection. We might match point by point and find the 2 closest. I used a different approach: for each point of an edge, find the closer to the average of the 2 vertices of the other edges (A and B represents the 2 edges):


if distance(posA[1], avgB)< distance(posA[0], avgB):

 closerA=vtxsA[1]

 if distance(posB[1], avgA)< distance(posB[0], avgA):

   closerB=vtxsB[1]

We can compute the intersection between 2 lines in the 3d space using  different approaches, according the way we choose to represent them: e.g. intersection between planes, parametric equations and so on.

I went for parametric equations:

For S:

X(t) = x0 + (x1-x0)*t

Y(t) = y0 + (y1-y0)*t

Z(t) = z0 + (z1-z0)*t

For R:

X’(h) = x’0 + (x’1-x’0)*h

Y’(h) = y’0 + (y’1-y’0)*h

Z’(h) = z’0 + (z’1-z’0)*h

with l=x1-x0, l’=x1′-x0′, m=y1-y0, m’= .., n=.., n’=..,  as the director parameter of the lines.

From the parametric equations we can compute the vector difference:

D(t,h) = [Dx(t,h), Dy(t,h), Dz(t,h)]

with:

Dx = X’(h) – X(t), Dy=Y’(h) – Y(t), Dz=Z’(h)-Z(t)

We can study the gradient of D(t,h) to find the minimum value, i.e. :

This give us a system in 2 equations with 2 variables (t and h).

According the solutions given by the system we can see if the lines intersect, are parallel or crooked. The rest of the code is raw math to translate all these concepts.

The final lines assign the corresponding coordinates given by h and t to the closest vertices to the corner:


PA_X = qA + t*lA

PA_Y = uA + t*mA

PA_Z = wA + t*nA

PB_X = qB + h*lB

PB_Y = uB + h*mB

PB_Z = wB + h*nB

...

cmds.xform(closerA, t=[PA_X, PA_Y, PA_Z], ws=1)
cmds.xform(closerB, t=[PB_X, PB_Y, PB_Z], ws=1)

You can download the sourcecode here:

mergeAngle.zip

To test it, source the contained .py file, select 2 edges as shown in picture and call the function mergeAngle()


The code is pretty rough but effective.. just coded in half an hour. It wants to be a start point to work on. A better implementation would include iterate along all the border vertices to get the corresponding edges and fix them at a glance.. any suggestion, request, improvement is very welcome.


Thanks for reading