Intersection of a curve and a mesh

hi,

I am trying to think of any function that can give me the intersection point of a curve and a mesh,
I know ways to find an intersection between poly mesh, but not curve with mesh.
anyone can give a hint, thanks for the help

I didn’t see anything in the api just sitting there ready to go in MFnMesh or MFnNurbsCurve.

You could start by testing if the bboxes intersect. Then, seems like you’d take the start of the curve, find the first and second derivative, shoot a ray, and see if it intersects the mesh, walk along the curve until there is an intersection, then store the distance to the intersection that is small enough to consider it touching. Then find an efficient algorithm to find the limit.

There are some super clever bifrost folks out there, so that might be more fun to prototype there than in a plugin.

This problem does not have an uncompromising “solution in a general way.”
But for specific cases and situations, hyper-efficient solutions can be implemented.
But, there are a lot of BUT.
Is the target geometry a plane?
Is it known in advance how many times the curve can intersect the geometry?
Need to calculate all intersections or just the first one, or any one?
What is the total number of curves to track intersections?
What is the degree of curves?
How many EPs are on the curves?
How accurately do you need to determine the coordinates of intersections?
If you need to track in real time the intersections of 1,000,000 curves (with several hundred EPs per curve) with complex (and possibly self-intersecting) geometry, such as the intersection of long hair with clothes in super close-ups, then this is one class of problems.
And if you need to track the intersection with the plane, then this is trivial.
It may be important whether you use Maya in standalone mode or in interactive mode.
If the requirements are very high, then most likely it will be necessary to additionally use third-party solutions based on the GPU. In the simplest case, something like PhisX .
So, all the details and nuances of the problem being solved are important. Preferably from a real problem.
With detailed requirements.
Then solutions will immediately appear. :slight_smile:

thanks for the information, I am trying to make a tool to project( extend) edge to a plane,
I was thinking of maybe creating a curve and scaling it so it intersects with the target mesh then finding out the intersection point.

Currently looking into using closestIntersection, but I am still learning API.



##################################################
import maya.cmds as mc
import maya.OpenMaya as om

cvPosBase = mc.pointPosition(‘pCylinder1.vtx[17]’)
mPoint = om.MPoint(cvPosBase[0],cvPosBase[1],cvPosBase[2])
cvPosAim = mc.pointPosition(‘pCylinder1.vtx[1]’)

V1 = om.MVector(*cvPosBase)
V2 = om.MVector(*cvPosAim)
dir = (V1 - V2).normal()
tarMeshList=[]
tarMeshList.append(‘pPlane1’)
tarMesh = tarMeshList[0]
hitpoint = om.MFloatPoint()
selectionList = om.MSelectionList()
selectionList.add(tarMesh)
dagPath = om.MDagPath()
selectionList.getDagPath(0, dagPath)
fnMesh = om.MFnMesh(dagPath)
intersection = fnMesh.closestIntersection(
om.MFloatPoint(mPoint),
om.MFloatVector(dir),
None,
None,
False,
om.MSpace.kWorld,
99999,
False,
None,
hitpoint,
None,
None,
None,
None,
None)

You could greatly optimize your code for a closestIntersection function or the like.
But, specifically for this case, it would be excessive and redundant.
This case is the most trivial: “the intersection of a line and a plane”.
It is detailed in the Maya Python API 2 examples:
https://help.autodesk.com/view/MAYAUL/2023/ENU/?guid=Maya_SDK_py_ref_python_2api2_2py2_square_scale_manip_context_8py_example_html
And again, it is not clear why you need to do this exactly (unless, of course, this is an arbitrary example for pumping programming skills).
Perhaps, if the ultimate goal were known, the problem could be reformulated to find the optimal method for solving the problem.
Also, if you intend to use the Maya Python API, it makes sense to use the new Python API 2.
Yes, many things are documented ugly and with errors, but it’s worth it to be persistent and make an effort. :slight_smile:
Good luck and harmony!

thanks for the help, I will take a deep look,
the tool I try to make will help a lot for the poly modelling task.
I can achieve the result with some hacking to the old paint effect,
but sometimes it is not very accurate.

here is a mock-up of the tool

Concerning the code resulted by you for closestIntersection.
This code cannot work correctly.

  1. wrong quotes. Possibly due to incorrect formatting.

  2. In this context, useless code:

tarMeshList=[]
tarMeshList.append("pPlane1")
tarMesh = tarMeshList[0]

  1. dir = (V1 - V2).normal()
    You specify direction in the opposite direction.
    And even with such an error, it could work,
    if you set the testBothDirections parameter to True :
intersection = fnMesh.closestIntersection(
                                            om.MFloatPoint(mPoint),
                                            om.MFloatVector(dir),
                                            None,
                                            None,
                                            False,
                                            om.MSpace.kWorld,
                                            99999,
                                            True,    # testBothDirections
                                            None,
                                            hitpoint,
                                            None,
                                            None,
                                            None,
                                            None,
                                            None
                                          )

In this case, the intersection would be searched in both directions.
And the position of the first found intersection would be returned:
Let’s see the result:
hitpoint
# Result: <maya.OpenMaya.MFloatPoint; proxy of <Swig Object of type 'MFloatPoint *' at 0x000001D06EE32660> >
Oops, to “get” the values you have to use the monstrous monster - MScriptUtil…

Okay, let’s use “as is”:
But first you have to convert MFloatPoint to MPoint

m_point = om.MPoint(hitpoint)
# Result: <maya.OpenMaya.MPoint; proxy of <Swig Object of type 'MPoint *' at 0x000001D0896F2540> >
selectionList.clear()
selectionList.add("pCylinder1")
dagPath = om.MDagPath()
selectionList.getDagPath(0, dagPath)
fnMesh = om.MFnMesh(dagPath)
fnMesh.setPoint(17, m_point, 4)
fnMesh.updateSurface()

Regarding a simple method for finding the intersection of a ray with a plane.
I will illustrate the mathematical method for finding the intersection of a ray with a plane for your example:

import maya.cmds as cmds
import maya.api.OpenMaya as om
maya_useNewAPI = True

## Let's create a polygonal plane and a polygonal cylinder without caps.
## Let's transform them to look like your sample.

poly_cyl = (cmds.polyCylinder(name="sourse_poly_000", r=10, h=20, sa=10, sh=1, sc=0, ax=(0, 1, 0), rcp=0, cuv=3, ch=0))[0]
# Result: 'sourse_poly_000'
cmds.delete( "{}.f[{}:{}]".format(poly_cyl,10, 11) )
cmds.move(0.0, 10.0, 0.0, poly_cyl, r=1)
cmds.scale(1.25, 1.0 ,1.25, "{}.e[{}:{}]".format(poly_cyl, 10, 19))
poly_plane = (cmds.polyPlane(name="target_poly_000", h=50, w=50, sh=10, sw=10, ax=(0, 1, 0), cuv=1, ch=0))[0]
# Result: 'target_poly_000'
cmds.move(0.0, 40.0, 0.0, poly_plane, r=1)
cmds.rotate(0.0, 0.0, 30.0, poly_plane, r=1)


## We need the normal (as a vector: "N") of our intersection plane (poly_plane "target_poly_000").
## And any point ("plane_point") on this plane (as its position: "plane_point_position")
## Let's create a Selection List and add our plane to it:

sel_list = om.MSelectionList()
sel_list.add(poly_plane)
# Result: <OpenMaya.MSelectionList object at 0x000001F5D58CB250>

## Get the DagPath for the contents of the Selection List
dag_path_plane = sel_list.getDagPath(0)
# Result: <OpenMaya.MDagPath object at 0x000001F5D58B0FB0>

## Let's create an `OpenMaya MFnMesh` object for our  plane (passing our plane's DagPath as a parameter).

fn_mesh_plane = om.MFnMesh(dag_path_plane)
# Result: <OpenMaya.MFnMesh object at 0x000001F5D58CB6F0>

## We obtain for our plane the normal vector in world space,
## since our plane could be transformed.
## (Yes, our plane has been rotated and shifted)
## To get the normal of a plane, we can request a normal for any face of that plane.
## For example for "target_poly_000.f[0]".

N = fn_mesh_plane.getPolygonNormal(0, om.MSpace.kWorld)
# Result: maya.api.OpenMaya.MVector(-0.49999999999999994449, 0.86602540378443870761, 0)
## "0" - face ID from "target_poly_000.f[0]".World Space: "om.MSpace.kWorld" or "4" (om.MSpace.kWorld == 4)

## In addition, we need the normalized value of this normal vector.
N.normalize()
# Result: maya.api.OpenMaya.MVector(-0.49999999999999994449, 0.86602540378443870761, 0)
## we get the same value, since Maya uses normalized vector values for normals.

## We need the position of any point on the plane.
## In "world space"
## since our plane could be transformed.
## (Yes, our plane has been rotated and shifted)
## For example, take the position for the first vertex of this plane: "target_poly_000.vtx[0]"

plane_point = "target_poly_000.vtx[0]"

plane_point_position = fn_mesh_plane.getPoint(0, 4)
# Result: maya.api.OpenMaya.MPoint(-21.650635094610969134, 27.5, 25, 1)
## "0" - verttex ID from "target_poly_000.vtx[0]".World Space: "om.MSpace.kWorld" or "4" (om.MSpace.kWorld == 4)

## "N" and "plane_point_position" only need to be calculated once.
## These values will be used to calculate the intersections for all rays.

## We want to get the coordinates of the intersection point of the plane and the ray.
## A ray coming out of "in_point_01" (vertex "0" of our cylinder "sourse_poly_000")
## and passing through "in_point_02" (vertex "10" of our cylinder).

in_point_01 ="sourse_poly_000.vtx[0]"
in_point_02 ="sourse_poly_000.vtx[10]"

## Instead of creating a new Selection List,
## clear the already existing Selection List "sel_list".
## And add our cylinder to it (poly_cyl "sourse_poly_000").
sel_list.clear()
sel_list.add(poly_cyl)

## Set Dag Path for Select List Content
dag_path_cyl = sel_list.getDagPath(0)


## Let's create an MFnMesh object for our OpenMaya plane (passing our cylinder's DagPath as a parameter).

fn_mesh_cyl = om.MFnMesh(dag_path_cyl)


## Get the positions ("ray_position" and "dir_position") of the vertices ("sourse_poly_000.vtx[0]",
## "sourse_poly_000.vtx[10]") in world space:

ray_position = fn_mesh_cyl.getPoint(0, 4)
# Result: maya.api.OpenMaya.MPoint(8.0901708602905273438, 0, -5.8778543472290039062, 1)
dir_position = fn_mesh_cyl.getPoint(10, 4)
# Result: maya.api.OpenMaya.MPoint(13.961696624755859375, 48.060791015625, -10.143764495849609375, 1)

## Let's get a normalized direction vector ("ray_direct") for our ray coming out of the point "ray_position".
## To calculate the direction, we subtract from the direction point "dir_position" the starting point
## of the ray "ray_position", (as if they were vectors) and interpret the result as the value of a vector.

ray_direct = om.MVector( dir - ray ).normalize()
# Result: maya.api.OpenMaya.MVector(0.1207991613430363792, 0.98878953802889624214, -0.087765665857559654883)

## Now we turn to the calculation of intersections.
## These calculations actively use "Dot Produkt" or "Scalar Product", the so-called "Scalar Multiplication".
## This is an algebraic operation that takes two sequences of numbers of the same length,
## such as coordinate vectors, and returns the value as a single number.
## Python didn't support "Dot Produkt" until version 3.5!
## This is a very simple sequence of operations.
## And for it, you can write your own function in Python.
## But you should not do this, since the Maya API already implements scalar vector multiplication directly.
## For example: "Dot(V1, V2)". In the Maya API it's just: "(V1 * V2)"

ratio = N * ray_direct
# Result: 0.7959172782577852

denominator = N * om.MVector( plane_point_position - ray_position )
# Result: 38.68610158152281

## Verify that the vector and the plane are not parallel:
if denominator > .00001: intersection = ray_position + ray_direct * denominator / ratio
else: print("Ray direction and plane - parallel !")
# Result: maya.api.OpenMaya.MPoint(13.961696399865353158, 48.060789174806089363, -10.143764332457209321, 1)

## Let's check the result.
## move our vertex responsible for "dir_position" (in_point_02 "sourse_poly_000.vtx[10]")
## to the position of the intersection point:

fn_mesh_cyl.setPoint(10, intersection, 4)

## setPoint(10, intersection, 4).
## "10" - verttex ID from "target_poly_000.vtx[10]"
## "intersection" - target position (OpenMaya.MPoint):
## (13.961696399865353158, 48.060789174806089363, -10.143764332457209321, 1)
## "4" - World Space in MSpace: "om.MSpace.kWorld" or "4" (om.MSpace.kWorld == 4)

## Or, for clarity, let's create a locator at the position of the intersection point:

sp_locator = cmds.spaceLocator(p=(intersection.x, intersection.y, intersection.z), n="Intersection_000", a=1)
om.MGlobal.clearSelectionList()
cmds.setAttr("{}.localScale".format(sp_locator[0]), 2.0,2.0,2.0, type="double3")
cmds.setAttr("{}Shape.overrideEnabled".format(sp_locator[0]), True)
cmds.setAttr("{}Shape.overrideColor".format(sp_locator[0]), 9)

We have verified that this method works.
Yes, but how much more efficient and reliable is this method than the universal “closestIntersection” (without activation of acceleration methods)?
First, we modernize the code to automate the process (for a cylinder with an arbitrary number of edges-rays)
Let’s include two functions for finding intersections in the code.
Let’s provide an opportunity to switch between methods.
The first one will use the “closestIntersection” method (without activating the acceleration methods).
The second will use the classical mathematical method.

import maya.cmds as cmds
import maya.api.OpenMaya as om
try: from time import perf_counter as t_clock
except ImportError: from time import clock as t_clock
from timeit import timeit
maya_useNewAPI = True

def create_cilynder(ray_count):
    poly_cyl = (cmds.polyCylinder(name="sourse_poly_000", r=10, h=20, sa=ray_count, sh=1, sc=0, ax=(0, 1, 0), rcp=0, cuv=3, ch=0))[0]
    om.MGlobal.clearSelectionList()
    cmds.delete( "{}.f[{}:{}]".format(poly_cyl,ray_count, ray_count + 1) )
    cmds.move(0.0, 10.0, 0.0, poly_cyl, r=1)
    cmds.scale(1.25, 1.0 ,1.25, "{}.e[{}:{}]".format(poly_cyl,ray_count,ray_count*2-1))
    return poly_cyl


def create_poly_plane(height, weight, division, translation, rotatation):
    poly_plane = (cmds.polyPlane(name="target_poly_000", h=height, w=weight, sh=division, sw=division, ax=(0, 1, 0), cuv=1, ch=0))[0]
    om.MGlobal.clearSelectionList()
    cmds.move(translation[0], translation[1], translation[2], poly_plane, r=1)
    cmds.rotate(rotatation[0], rotatation[1], rotatation[2], poly_plane, r=1)
    return poly_plane


def fn_mesh(name):
    sel_list = om.MSelectionList()
    sel_list.add(name)
    dag_path = sel_list.getDagPath(0)
    fn_mesh = om.MFnMesh(dag_path)
    return fn_mesh


def create_ray_dir_array(poly_cyl, ray_count):
    sel_list = om.MSelectionList()
    sel_list.add("{}.e[{}:{}]".format(poly_cyl, ray_count*2, ray_count*3-1))
    node, component = sel_list.getComponent(0)
    it = om.MItMeshEdge(node, component)
    edges_count_in = it.count()
    ray_float_point_array = om.MFloatPointArray()
    dir_float_vector_array = om.MFloatVectorArray()
    while not it.isDone():
        point_0 = om.MFloatPoint(it.point(0))
        point_1 = om.MFloatPoint(it.point(1))
        ray_float_point_array.append(point_0)
        vector_d = om.MFloatVector(point_1 - point_0).normalize()
        dir_float_vector_array.append(vector_d)
        it.next()
    return ray_float_point_array, dir_float_vector_array


def intersections(poly_cyl, ray_count):
    time_00 = t_clock()
    fn_mesh_target = fn_mesh(poly_plane)
    arrays = create_ray_dir_array(poly_cyl, ray_count)
    ray_float_point_array = arrays[0]
    dir_float_vector_array = arrays[1]
    k_space = om.MSpace.kWorld
    testBothDirections = False
    k_tolerance = 0.001
    maxParam = 100.0    # As the maximum possible value of this parameter,
                        # it would be correct to pre-calculate the diagonal of the overall boundingbox.
                        # But we will set just a reasonable value with a little "margin"
    gen_list_inter = om.MPointArray()
    for i in range (ray_count):
        raySource = ray_float_point_array[i] # start point for ray search intersects. current edge(0)
        rayDir = dir_float_vector_array[i] # calculate direction for search intersects. Vector from current edge(0) to current edge(1)
        hit = fn_mesh_target.closestIntersection( raySource,
                                                  rayDir,
                                                  k_space,
                                                  maxParam,
                                                  testBothDirections,
                                                  #faceIds, triIds, idsSorted, accelParams,
                                                  # If you do not use these parameters,
                                                  # then simply do not specify them.
                                                  # You do not need to specify the value "None" !!!
                                                  # Otherwise it will cause errors or Maya crash.
                                                  tolerance = k_tolerance
                                                 )
        if hit and hit[0]: # if no hits, in Maya <=2020: return [], Maya>2020: return None
            hit_point, hit_ray_param, hit_face, hit_triangle, hit_bary_1, hit_bary_2 = hit
            gen_list_inter.append(hit_point)
    time_00 = t_clock() - time_00
    print("\n\t\t Total search intersects time for {} rays: {} s. (\"closestIntersection\" metod)\n".format(ray_count,time_00))
    return gen_list_inter


def target_const(poly_plane):
    fn_mesh_plane = fn_mesh(poly_plane)
    N = om.MFloatVector(fn_mesh_plane.getPolygonNormal(0, om.MSpace.kWorld).normalize())
    plane_point = "{}.vtx[0]".format(poly_plane)
    plane_point_position = om.MFloatPoint(fn_mesh_plane.getPoint(0, 4))
    return N, plane_point_position


def intersections_simply(poly_cyl, poly_plane, ray_count):
    time_00 = t_clock()
    arrays = create_ray_dir_array(poly_cyl, ray_count)
    ray_float_point_array = arrays[0]
    dir_float_vector_array = arrays[1]
    N, plane_point_position = target_const(poly_plane)
    gen_list_inter = om.MPointArray()
    for i in range (ray_count):
        ray_direct = dir_float_vector_array[i]
        ray_position = ray_float_point_array[i]
        ratio = N * ray_direct
        denominator = N * om.MFloatVector( plane_point_position - ray_position )
        if denominator > .00001:
            intersection = ray_position + ray_direct * denominator / ratio
        else: cmds.error("Ray direction and plane - parallel !")
        gen_list_inter.append(intersection)
    time_00 = t_clock() - time_00
    print("\n\t\t Total search intersects time for {} rays: {} s. (\"Math Intersection\" metod)\n".format(ray_count,time_00))
    return gen_list_inter


def move_vertices(gen_list_inter, ray_count, poly_cyl):
    if len(gen_list_inter) == ray_count:
        fn_mesh_cyl=fn_mesh(poly_cyl)
        for i in range(ray_count):
            fn_mesh_cyl.setPoint(ray_count + i, gen_list_inter[i], 4)


def locators_for_intersects(gen_list_inter, ray_count):
    if len(gen_list_inter) == ray_count:
        for item in gen_list_inter:
            sp_locator = cmds.spaceLocator(p=(item.x, item.y, item.z), n="Intersection_000", a=1)
            om.MGlobal.clearSelectionList()
            cmds.setAttr("{}.localScale".format(sp_locator[0]), 2.0,2.0,2.0, type="double3")
            cmds.setAttr("{}Shape.overrideEnabled".format(sp_locator[0]), True)
            cmds.setAttr("{}Shape.overrideColor".format(sp_locator[0]), 9)


def general(ray_count, method=1):
    poly_cyl = create_cilynder(ray_count)
    poly_plane = create_poly_plane(50.0, 50.0, 10, (0.0,40.0,0.0), (0.0,0.0,30.0))
    if method !=0:
        gen_list_inter = intersections_simply(poly_cyl, poly_plane, ray_count)
    else:
        gen_list_inter = intersections(poly_cyl, ray_count)

    move_vertices(gen_list_inter, ray_count, poly_cyl)
    locators_for_intersects(gen_list_inter, ray_count)


general(50) ## "Math Intersection" metod. Or "general(50, 0)" - "closestIntersection" metod
            ## !!! Maya does not allow you to create a cylinder with more than 1000 divisions !!!

For 1000 intersections, without data preparation
(Time spent only on calculation of intersection. Without data preparation ):

general(1000, 0)
# Total search intersects time for 1000 rays: 0.0131282000002102 s. ("closestIntersection" metod)

general(1000)
# Total search intersects time for 1000 rays: 0.0029899000001024 s. ("Math Intersection" metod)

“Math Intersection” metod ~ 4.5 times faster

####################################################

For 5,000,000 intersections, including data preparation overhead:

ray_count = 1000
timeit(lambda: intersections(poly_cyl, ray_count), number = 5000)
# Result: 88.27569920000133 s. ("closestIntersection" metod)
timeit(lambda: intersections_simply(poly_cyl, ray_count), number = 5000)
# Result: 37.94165220000105 s. ("Math Intersection" metod)

“Math Intersection” metod ~ 2.5 times faster

#####################################################

Obviously, the mathematical method does not look for intersections within the boundaries of our plane.
Intersections are searched for an arbitrary infinite plane drawn through the specified point and oriented according to the specified normal.
This quality may be indispensable in some cases, but may be a hindrance in others.

wow, thanks for the information, very deep and clear.
I learned more about Maya API now,
it will take a while for me to absorb everything.
sometimes I know what I want to do, but have zero ideas how to code it.
still learning python, and API is another level.
Once again. thank you for your time,
This information will also help many other people like me.